gloomberb 0.17.4 → 0.17.6
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 +3 -1
- package/patches/electrobun@1.18.1.patch +80 -0
- package/src/api-client/chat.ts +9 -0
- package/src/api-client/commodity-balances.ts +77 -0
- package/src/api-client/data.ts +35 -3
- package/src/api-client/earnings.ts +10 -0
- package/src/api-client/index.ts +5 -0
- package/src/api-client/insider-signals.ts +75 -0
- package/src/api-client/market-discovery.ts +1 -1
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/paths.ts +9 -0
- package/src/api-client/research-activity.ts +10 -3
- package/src/api-client/socket.ts +10 -0
- package/src/api-client/types.ts +25 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
- package/src/app/runtime/broker-import.ts +23 -21
- package/src/app/runtime/plugin-bindings.ts +13 -7
- package/src/app/runtime/update.ts +64 -4
- package/src/app.tsx +2 -0
- package/src/architecture/core-builtin-allowlist.json +2 -10
- package/src/ask-gloom/pane-context.ts +11 -1
- package/src/brokers/broker-portfolio-sync.ts +66 -0
- package/src/brokers/sync-broker-instance.ts +75 -41
- package/src/cli/commands/market.ts +101 -34
- package/src/cli/commands/options-chain.ts +20 -6
- package/src/cli/commands/overview.ts +54 -13
- package/src/cli/commands/search.ts +1 -1
- package/src/cli/commands/system.ts +19 -1
- package/src/cli/commands/ticker.ts +24 -4
- package/src/cli/context.ts +13 -0
- package/src/cli/errors.ts +15 -2
- package/src/cli/fx-pair.ts +77 -0
- package/src/cli/glossary.ts +34 -4
- package/src/cli/history-rows.ts +25 -11
- package/src/cli/index.ts +30 -12
- package/src/cli/listing-arg.ts +61 -138
- package/src/cli/listing-suggestion.ts +3 -21
- package/src/cli/pane-functions/catalog.ts +50 -0
- package/src/cli/pane-functions/freshness.ts +44 -9
- package/src/cli/pane-functions/function-help.ts +9 -9
- package/src/cli/pane-functions/headless.ts +19 -7
- package/src/cli/pane-functions/index.ts +63 -5
- package/src/cli/pane-functions/market-session.ts +18 -3
- package/src/cli/pane-functions/resolver.ts +29 -8
- package/src/cli/pane-functions/screenshot.ts +2 -0
- package/src/cli/report-tables.ts +11 -4
- package/src/cli/result.ts +2 -0
- package/src/cli/series-search.ts +6 -1
- package/src/cli/ticker-resolution.ts +1 -2
- package/src/cli/unknown-command.ts +18 -0
- package/src/components/chart/composite/composite-chart.tsx +80 -19
- package/src/components/chart/composite/format.ts +17 -0
- package/src/components/chart/composite/legend.tsx +26 -15
- package/src/components/chart/composite/panel-labels.ts +38 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +25 -7
- package/src/components/chart/composite/price-axis-labels.tsx +16 -3
- package/src/components/chart/composite/scene.ts +60 -2
- package/src/components/chart/composite/series-axis-tags.tsx +67 -0
- package/src/components/chart/composite/tools.ts +2 -2
- package/src/components/chart/composite/types.ts +28 -0
- package/src/components/chart/core/view-reset.ts +46 -0
- package/src/components/command-bar/commands/collection.ts +1 -1
- package/src/components/command-bar/commands/direct/index.ts +48 -1
- package/src/components/command-bar/commands/registry.ts +8 -0
- package/src/components/command-bar/list/model.ts +7 -0
- package/src/components/command-bar/pane-templates/workflow.ts +5 -0
- package/src/components/command-bar/panel/prompt-binding.ts +8 -0
- package/src/components/command-bar/routes/root/command-items.ts +17 -2
- package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
- package/src/components/command-bar/routes/root/provider-search.ts +6 -0
- package/src/components/command-bar/routes/root/runtime.ts +3 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
- package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
- package/src/components/command-bar/surface/index.tsx +7 -1
- package/src/components/command-bar/workflow/collection-actions.ts +6 -5
- package/src/components/command-bar/workflow/ops.ts +8 -5
- package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
- package/src/components/command-bar/workflow/tickers.ts +1 -1
- package/src/components/form-modal/confirm.tsx +1 -0
- package/src/components/form-modal/request.ts +2 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -1
- package/src/components/layout/pane/content.tsx +11 -8
- package/src/components/layout/pane/sidebar.tsx +24 -11
- package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
- package/src/components/layout/shell/index.tsx +5 -1
- package/src/components/layout/shell/menu.ts +8 -0
- package/src/components/layout/shell/pane/capture.ts +29 -12
- package/src/components/onboarding/wizard-model.ts +1 -1
- package/src/components/onboarding/wizard-positions.ts +18 -10
- package/src/components/ticker/badge/format.ts +64 -15
- package/src/components/ticker/badge/index.tsx +32 -11
- package/src/components/ticker/badge/list.tsx +4 -3
- package/src/components/ticker/badge/text.tsx +2 -2
- package/src/components/ticker/list-table-view.tsx +8 -1
- package/src/components/ui/confirm-dialog.tsx +30 -4
- package/src/components/ui/data-table/opentui/index.tsx +61 -20
- package/src/components/ui/data-table/opentui/model.ts +104 -0
- package/src/components/ui/fields.tsx +5 -0
- package/src/components/ui/inline-quick-add.tsx +4 -1
- package/src/components/ui/query-bar-keys.tsx +29 -5
- package/src/components/ui/query-bar.tsx +29 -4
- package/src/components/ui/windowed-list.tsx +27 -0
- package/src/data/config/store/normalize.ts +8 -0
- package/src/docs/snapshot.json +248 -5
- package/src/i18n/es.ts +3 -1
- package/src/i18n/ja.ts +3 -1
- package/src/i18n/ko.ts +3 -1
- package/src/i18n/zh-cn.ts +3 -1
- package/src/i18n/zh-tw.ts +3 -1
- package/src/layout-marketplace/cloud.ts +16 -5
- package/src/layout-marketplace/gallery.tsx +4 -4
- package/src/layout-marketplace/linked-sync.ts +9 -3
- package/src/layout-marketplace/linked.ts +24 -3
- package/src/layout-marketplace/model.ts +39 -4
- package/src/market-data/coordinator/events.ts +5 -1
- package/src/market-data/coordinator/index.ts +38 -1
- package/src/market-data/coordinator/quotes.ts +4 -0
- package/src/market-data/futures-contract.ts +53 -0
- package/src/market-data/hooks.tsx +225 -30
- package/src/market-data/market/bar-period.ts +71 -0
- package/src/market-data/market/format.ts +14 -2
- package/src/market-data/market/freshness.ts +28 -1
- package/src/market-data/market/status.ts +152 -13
- package/src/market-data/model-dividend-yield.ts +76 -0
- package/src/market-data/quotes/freshness.ts +30 -1
- package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/plugins/builtin/analytics/metrics.ts +8 -4
- package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
- package/src/plugins/builtin/analytics/risk-model.ts +74 -15
- package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
- package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
- package/src/plugins/builtin/broker-manager/model.ts +2 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
- package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
- package/src/plugins/builtin/chart-composer/formula.ts +233 -0
- package/src/plugins/builtin/chart-composer/headless.ts +27 -2
- package/src/plugins/builtin/chart-composer/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +41 -10
- package/src/plugins/builtin/chart-composer/presets.ts +91 -2
- package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
- package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
- package/src/plugins/builtin/chart-composer/settings.ts +16 -4
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
- package/src/plugins/builtin/chat/content/footer.tsx +5 -2
- package/src/plugins/builtin/chat/content/index.tsx +12 -5
- package/src/plugins/builtin/chat/content/mentions.ts +50 -21
- package/src/plugins/builtin/chat/content/scroll.ts +12 -5
- package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
- package/src/plugins/builtin/chat/controller/index.ts +88 -17
- package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
- package/src/plugins/builtin/chat/controller/messages.ts +14 -2
- package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
- package/src/plugins/builtin/chat/controller/state.ts +25 -0
- package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
- package/src/plugins/builtin/chat/controller/view.ts +4 -0
- package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
- package/src/plugins/builtin/chat/conversations.ts +12 -5
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/layout.ts +23 -5
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/chat/share/flow.tsx +18 -9
- package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
- package/src/plugins/builtin/chat/sidebar.tsx +6 -1
- package/src/plugins/builtin/chat/unread-badge.ts +38 -7
- package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
- package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
- package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
- package/src/plugins/builtin/commodity-balances/client.ts +51 -193
- package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
- package/src/plugins/builtin/commodity-balances/model.ts +5 -180
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
- package/src/plugins/builtin/custom-view/index.tsx +1 -1
- package/src/plugins/builtin/custom-view/loader.ts +57 -7
- package/src/plugins/builtin/custom-view/sources.ts +146 -0
- package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
- package/src/plugins/builtin/earnings/board-model.ts +33 -3
- package/src/plugins/builtin/earnings/board-table.ts +59 -14
- package/src/plugins/builtin/earnings/board.tsx +36 -3
- package/src/plugins/builtin/earnings/headless.ts +24 -5
- package/src/plugins/builtin/earnings/history-model.ts +9 -1
- package/src/plugins/builtin/earnings/history-table.ts +16 -5
- package/src/plugins/builtin/earnings/history.tsx +40 -10
- package/src/plugins/builtin/earnings/index.tsx +3 -1
- package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
- package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
- package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
- package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
- package/src/plugins/builtin/econ-statistics/view.ts +4 -0
- package/src/plugins/builtin/futures/model.ts +5 -4
- package/src/plugins/builtin/futures/series.ts +38 -1
- package/src/plugins/builtin/futures-curve/client.ts +4 -4
- package/src/plugins/builtin/futures-curve/headless.ts +33 -8
- package/src/plugins/builtin/futures-curve/index.tsx +4 -4
- package/src/plugins/builtin/futures-curve/model.ts +109 -6
- package/src/plugins/builtin/futures-curve/pane.tsx +24 -13
- package/src/plugins/builtin/insider/headless.ts +74 -4
- package/src/plugins/builtin/insider/index.tsx +99 -5
- package/src/plugins/builtin/insider/signals-client.ts +69 -0
- package/src/plugins/builtin/insider/signals-model.ts +146 -0
- package/src/plugins/builtin/insider/signals.tsx +176 -0
- package/src/plugins/builtin/iv-history/client.ts +41 -2
- package/src/plugins/builtin/iv-history/headless.ts +24 -16
- package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
- package/src/plugins/builtin/jodi/client.ts +42 -205
- package/src/plugins/builtin/jodi/headless.ts +13 -4
- package/src/plugins/builtin/jodi/model.ts +18 -43
- package/src/plugins/builtin/jodi/pane.tsx +5 -0
- package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
- package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/macro-day/release-table.ts +3 -3
- package/src/plugins/builtin/market-movers/client.ts +32 -6
- package/src/plugins/builtin/market-movers/headless.ts +7 -2
- package/src/plugins/builtin/market-movers/model.ts +64 -3
- package/src/plugins/builtin/market-movers/screener.ts +20 -9
- package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
- package/src/plugins/builtin/market-valuation/headless.ts +17 -4
- package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
- package/src/plugins/builtin/market-valuation/view.ts +10 -0
- package/src/plugins/builtin/mna/model.ts +2 -1
- package/src/plugins/builtin/options-positioning/client.ts +10 -0
- package/src/plugins/builtin/options-positioning/headless.ts +88 -13
- package/src/plugins/builtin/options-positioning/index.tsx +2 -2
- package/src/plugins/builtin/options-positioning/model.ts +47 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +55 -26
- package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
- package/src/plugins/builtin/options-scenario/client.ts +111 -17
- package/src/plugins/builtin/options-scenario/collar.ts +141 -0
- package/src/plugins/builtin/options-scenario/csp.ts +1 -1
- package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
- package/src/plugins/builtin/options-scenario/headless.ts +28 -12
- package/src/plugins/builtin/options-scenario/index.tsx +1 -1
- package/src/plugins/builtin/options-scenario/model.ts +55 -5
- package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
- package/src/plugins/builtin/perps/client.ts +77 -6
- package/src/plugins/builtin/perps/headless.ts +25 -14
- package/src/plugins/builtin/perps/index.tsx +1 -1
- package/src/plugins/builtin/perps/model.ts +32 -0
- package/src/plugins/builtin/perps/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
- package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
- package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
- package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
- package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
- package/src/plugins/builtin/rate-path/model.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +2 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
- package/src/plugins/builtin/research/event-model.ts +41 -49
- package/src/plugins/builtin/research/event-sources.ts +1 -1
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
- package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
- package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
- package/src/plugins/builtin/sectors/client.ts +46 -19
- package/src/plugins/builtin/sectors/sector-model.ts +22 -10
- package/src/plugins/builtin/shared/earnings-report.ts +48 -1
- package/src/plugins/builtin/shared/history-span.ts +55 -0
- package/src/plugins/builtin/shared/report-freshness.ts +20 -11
- package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
- package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
- package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
- package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
- package/src/plugins/builtin/themes/headless.ts +15 -2
- package/src/plugins/builtin/thirteenf/api.ts +60 -43
- package/src/plugins/builtin/thirteenf/data.ts +76 -35
- package/src/plugins/builtin/thirteenf/headless.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
- package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
- package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
- package/src/plugins/builtin/yield-curve/curves.ts +15 -1
- package/src/plugins/builtin/yield-curve/headless.ts +17 -7
- package/src/plugins/builtin/yield-curve/history.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +23 -18
- package/src/plugins/builtin/yield-curve/series.ts +19 -9
- package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
- package/src/plugins/builtin/yield-curve/world.ts +4 -0
- package/src/plugins/registry/host-actions.ts +3 -0
- package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
- package/src/portfolio/valuation-basis.ts +4 -21
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/remote/controller.ts +27 -7
- package/src/remote/schema.ts +25 -9
- package/src/renderers/browser/app-services.ts +7 -1
- package/src/renderers/browser/config-host.ts +55 -2
- package/src/renderers/browser/first-screen.ts +193 -0
- package/src/renderers/browser/main.tsx +59 -7
- package/src/renderers/cloudflare/http-proxy.ts +21 -13
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/desktop/query-bar.tsx +1 -1
- package/src/renderers/dom/dom-ui-host.tsx +2 -0
- package/src/renderers/dom/windowed-list.tsx +201 -0
- package/src/renderers/dom/windowed-rows.ts +65 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -7
- package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
- package/src/renderers/electrobun/bun/desktop/update.ts +132 -14
- package/src/renderers/electrobun/shared/protocol.ts +7 -4
- package/src/renderers/electrobun/view/http-fetch.ts +12 -1
- package/src/renderers/electrobun/view/update-host.ts +2 -2
- package/src/shares/bug-report-dialog.tsx +162 -0
- package/src/shares/bug-report-flow.tsx +84 -0
- package/src/shares/bug-report.ts +42 -0
- package/src/shares/capture-flow.ts +19 -0
- package/src/shares/capture-preview.tsx +129 -0
- package/src/shares/chat.ts +10 -1
- package/src/shares/dialog.tsx +16 -91
- package/src/shares/flow.tsx +12 -16
- package/src/shares/personal-pane.ts +35 -1
- package/src/sources/listing-symbols.ts +8 -4
- package/src/state/app/context/index.tsx +13 -0
- package/src/state/app/tab-cursor.ts +85 -0
- package/src/sync/core-contributors.ts +206 -59
- package/src/telemetry/usage-counts.ts +16 -0
- package/src/test-support/config-store.ts +31 -0
- package/src/test-support/moved-symbols.ts +64 -0
- package/src/theme/colors.ts +30 -0
- package/src/tickers/listing-fate.ts +242 -0
- package/src/tickers/open-target.ts +61 -11
- package/src/tickers/search/index.ts +45 -3
- package/src/tickers/search/ranking.ts +19 -2
- package/src/tickers/search/types.ts +3 -0
- package/src/tickers/search/upsert.ts +22 -7
- package/src/tickers/ticker-changes.ts +26 -9
- package/src/time-series/bar-spacing.ts +19 -0
- package/src/time-series/chart-data.ts +37 -13
- package/src/time-series/field-catalog.ts +5 -2
- package/src/time-series/forward-valuation.ts +10 -7
- package/src/time-series/fundamentals.ts +49 -11
- package/src/time-series/hooks.ts +2 -0
- package/src/time-series/live-bars.ts +3 -1
- package/src/time-series/parsed-history-cache.ts +2 -0
- package/src/time-series/price-units.ts +69 -0
- package/src/time-series/resolve.ts +71 -11
- package/src/time-series/session-bar.ts +138 -0
- package/src/time-series/spec.ts +20 -3
- package/src/time-series/studies.ts +211 -12
- package/src/time-series/transforms.ts +2 -0
- package/src/time-series/types.ts +11 -1
- package/src/time-series/valuation-price.ts +13 -0
- package/src/types/config.ts +24 -1
- package/src/types/financials.ts +2 -0
- package/src/types/headless.ts +27 -0
- package/src/types/plugin.ts +7 -0
- package/src/ui/host.tsx +5 -0
- package/src/updater/channel.ts +51 -0
- package/src/updater/github-releases.ts +85 -16
- package/src/updater/index.ts +128 -17
- package/src/utils/asset-category.ts +20 -0
- package/src/utils/datetime-format.ts +7 -1
- package/src/utils/decimal-comma.ts +83 -3
- package/src/utils/fundamentals.ts +7 -2
- package/src/utils/http-proxy-response.ts +66 -9
- package/src/utils/plugin-proxy-hosts.json +1 -5
- package/src/utils/semver.ts +47 -2
- package/src/utils/typo-distance.ts +22 -0
- package/src/plugins/builtin/shared/zip-text.ts +0 -46
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
import { Box, ScrollBox, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
|
|
2
|
+
import { Box, ScrollBox, Span, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
|
|
3
3
|
import { colors as themeColors, hoverBg } from "../../../theme/colors";
|
|
4
4
|
import { CANONICAL_EXCHANGE_ALIASES } from "../../../utils/exchanges";
|
|
5
5
|
import { displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
|
|
6
6
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
7
7
|
import { consumeChartMouseEvent, type ChartMouseEvent } from "../core/pointer";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
formatCompositeCarriedStamp,
|
|
10
|
+
formatCompositeDatasetDate,
|
|
11
|
+
formatCompositePointDetails,
|
|
12
|
+
formatCompositeSeriesValue,
|
|
13
|
+
seriesPriceReference,
|
|
14
|
+
} from "./format";
|
|
9
15
|
import type { CompositeChartProps, CompositeChartScene } from "./types";
|
|
10
16
|
import { seriesCueCssBackground, type SeriesLineCue } from "./series-cues";
|
|
11
17
|
|
|
@@ -45,15 +51,6 @@ function axisPriceReference(scene: CompositeChartScene | null, series: ResolvedS
|
|
|
45
51
|
return own;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
49
|
-
|
|
50
|
-
/** `Oct 4`, the axis's own wording, with the year when the chart ends in another. */
|
|
51
|
-
function legendDateText(date: Date, endTime: number | undefined): string {
|
|
52
|
-
const text = `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
53
|
-
const endYear = endTime === undefined ? null : new Date(endTime).getUTCFullYear();
|
|
54
|
-
return endYear === null || endYear === date.getUTCFullYear() ? text : `${text} ${date.getUTCFullYear()}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
54
|
/**
|
|
58
55
|
* A dataset's values arrive days late, so the legend dates them. Idle, one
|
|
59
56
|
* date leads the legend when it is every value's; otherwise, and under the
|
|
@@ -72,7 +69,7 @@ function legendValueDates(
|
|
|
72
69
|
valued += 1;
|
|
73
70
|
if (entry.observationKind === "dataset" && cursorValue.point) dates.set(entry.id, cursorValue.point.date);
|
|
74
71
|
}
|
|
75
|
-
const text = new Map([...dates].map(([id, date]) => [id,
|
|
72
|
+
const text = new Map([...dates].map(([id, date]) => [id, formatCompositeDatasetDate(date, scene?.endTime)] as const));
|
|
76
73
|
const cursorDate = scene?.cursorDate;
|
|
77
74
|
if (cursorDate) {
|
|
78
75
|
const cursorDay = cursorDate.toISOString().slice(0, 10);
|
|
@@ -191,6 +188,9 @@ export function CompositeLegend({
|
|
|
191
188
|
const ownDate = valueDates.own.get(entry.id);
|
|
192
189
|
// Carried from an earlier observation: another venue's closed session, a coarser series.
|
|
193
190
|
const carried = !!scene?.cursorDate && !!cursorValue?.carried && cursorValue.value != null;
|
|
191
|
+
// What a futures price counts, beside its number as FUT prints it.
|
|
192
|
+
const statedUnit = entry.points.length > 0 && !formatValue && entry.statedUnit ? ` ${entry.statedUnit}` : "";
|
|
193
|
+
const valueTail = `${ownDate ? ` ${ownDate}` : ""}${changeText}`;
|
|
194
194
|
const valueText = entry.points.length === 0
|
|
195
195
|
? entry.hidden ? "" : "no data"
|
|
196
196
|
: `${formatLegendSeriesValue(
|
|
@@ -198,7 +198,7 @@ export function CompositeLegend({
|
|
|
198
198
|
cursorValue?.value ?? null,
|
|
199
199
|
formatValue,
|
|
200
200
|
scene,
|
|
201
|
-
)}${
|
|
201
|
+
)}${statedUnit}${valueTail}`;
|
|
202
202
|
const fullText = [entry.label, valueText].filter(Boolean).join(" ");
|
|
203
203
|
const details = formatCompositePointDetails(cursorValue?.point);
|
|
204
204
|
const exactTotal = entry.unitGroup.split(":")[0] === "currency-total"
|
|
@@ -220,6 +220,8 @@ export function CompositeLegend({
|
|
|
220
220
|
width: Math.max(1, displayWidth(text)) + markerWidth,
|
|
221
221
|
labelWidth,
|
|
222
222
|
valueText,
|
|
223
|
+
statedUnit,
|
|
224
|
+
valueTail,
|
|
223
225
|
toggleable,
|
|
224
226
|
tooltip,
|
|
225
227
|
carried,
|
|
@@ -361,8 +363,9 @@ export function CompositeLegend({
|
|
|
361
363
|
data-gloom-role="composite-chart-legend-scroll"
|
|
362
364
|
>
|
|
363
365
|
<Box flexDirection="row" width={desiredSeriesWidth} height={1} gap={1}>
|
|
364
|
-
{entries.map(({ entry, cue, marker, text, toggleable, tooltip, carried, width: entryWidth }, index) => {
|
|
366
|
+
{entries.map(({ entry, cue, marker, text, statedUnit, valueTail, toggleable, tooltip, carried, width: entryWidth }, index) => {
|
|
365
367
|
const entryVisible = visibleSeriesIds.has(entry.id);
|
|
368
|
+
const unitAt = statedUnit ? text.length - statedUnit.length - valueTail.length : -1;
|
|
366
369
|
return (
|
|
367
370
|
<Box
|
|
368
371
|
key={entry.id}
|
|
@@ -396,7 +399,15 @@ export function CompositeLegend({
|
|
|
396
399
|
)}
|
|
397
400
|
{/* The filled/hollow marker already says whether a series is
|
|
398
401
|
shown; the word only repeated it in every legend slot. */}
|
|
399
|
-
<Text fg={entryVisible && !carried ? themeColors.text : themeColors.textDim}>
|
|
402
|
+
<Text fg={entryVisible && !carried ? themeColors.text : themeColors.textDim}>
|
|
403
|
+
{unitAt >= 0 ? (
|
|
404
|
+
<>
|
|
405
|
+
{text.slice(0, unitAt)}
|
|
406
|
+
<Span fg={themeColors.textDim}>{statedUnit}</Span>
|
|
407
|
+
{valueTail}
|
|
408
|
+
</>
|
|
409
|
+
) : text}
|
|
410
|
+
</Text>
|
|
400
411
|
</Box>
|
|
401
412
|
);
|
|
402
413
|
})}
|
|
@@ -39,6 +39,44 @@ export function resolvePanelCrosshair(
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface SeriesAxisTag {
|
|
43
|
+
seriesId: string;
|
|
44
|
+
seriesLabel: string;
|
|
45
|
+
side: "left" | "right";
|
|
46
|
+
value: number;
|
|
47
|
+
label: string;
|
|
48
|
+
color: string;
|
|
49
|
+
yRatio: number;
|
|
50
|
+
carried: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Each observation belongs to its own axis, never to the pointer's height. */
|
|
54
|
+
export function resolveSeriesAxisTags(
|
|
55
|
+
panel: CompositePanelScene,
|
|
56
|
+
scene: Pick<CompositeChartScene, "cursorXRatio" | "cursorValues">,
|
|
57
|
+
format?: CompositeAxisValueFormatter,
|
|
58
|
+
): SeriesAxisTag[] {
|
|
59
|
+
if (scene.cursorXRatio === null) return [];
|
|
60
|
+
const values = new Map(scene.cursorValues.map((entry) => [entry.seriesId, entry]));
|
|
61
|
+
return panel.series.flatMap(({ source }) => {
|
|
62
|
+
const cursor = values.get(source.id);
|
|
63
|
+
const domain = panel.axes[source.axis];
|
|
64
|
+
if (source.hidden || !domain || cursor?.value == null) return [];
|
|
65
|
+
const yRatio = projectCompositeValue(cursor.value, domain);
|
|
66
|
+
if (yRatio === null) return [];
|
|
67
|
+
return [{
|
|
68
|
+
seriesId: source.id,
|
|
69
|
+
seriesLabel: source.label,
|
|
70
|
+
side: source.axis,
|
|
71
|
+
value: cursor.value,
|
|
72
|
+
label: (format ?? formatCompositeCursorValue)(cursor.value, domain),
|
|
73
|
+
color: source.color,
|
|
74
|
+
yRatio,
|
|
75
|
+
carried: !!cursor.carried,
|
|
76
|
+
}];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
export function axisLabelRows(lines: string[]): ReadonlyMap<number, string> {
|
|
43
81
|
return new Map(lines.flatMap((line, row) => {
|
|
44
82
|
const label = line.trim();
|
|
@@ -28,6 +28,7 @@ function sameSeriesMeta(left: ResolvedSeries, right: ResolvedSeries): boolean {
|
|
|
28
28
|
&& left.color === right.color
|
|
29
29
|
&& left.unit === right.unit
|
|
30
30
|
&& left.unitGroup === right.unitGroup
|
|
31
|
+
&& left.statedUnit === right.statedUnit
|
|
31
32
|
&& left.priceAssetCategory === right.priceAssetCategory
|
|
32
33
|
&& left.nativeFrequency === right.nativeFrequency
|
|
33
34
|
&& left.timestampMode === right.timestampMode
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
type PanGesture,
|
|
68
68
|
type PendingWheel,
|
|
69
69
|
} from "./panel-gestures";
|
|
70
|
-
import { resolvePanelCrosshair, axisLabelRows, cursorAxisLabel, resolveSeriesCursorYRatio } from "./panel-labels";
|
|
70
|
+
import { resolvePanelCrosshair, axisLabelRows, cursorAxisLabel, resolveSeriesCursorYRatio, resolveSeriesAxisTags } from "./panel-labels";
|
|
71
71
|
import type { KeyboardToolDrag } from "./use-composite-chart-keyboard";
|
|
72
72
|
|
|
73
73
|
const webFrame = globalThis as typeof globalThis & {
|
|
@@ -101,6 +101,8 @@ interface CompositePanelSurfaceProps {
|
|
|
101
101
|
onSelectDrawing: (id: string | null) => void;
|
|
102
102
|
onActivate?: () => void;
|
|
103
103
|
onCursorDateChange: (date: Date | null) => void;
|
|
104
|
+
/** The cursor shown is another chart's, so the pointer's level is not part of it. */
|
|
105
|
+
sharedCursor: boolean;
|
|
104
106
|
/** Positive positions move toward older observations. A gesture supplies the frame and origin it started from. */
|
|
105
107
|
onPanViewport: (
|
|
106
108
|
deltaPositions: number,
|
|
@@ -147,6 +149,7 @@ export function CompositePanelSurface({
|
|
|
147
149
|
onSelectDrawing,
|
|
148
150
|
onActivate,
|
|
149
151
|
onCursorDateChange,
|
|
152
|
+
sharedCursor,
|
|
150
153
|
onPanViewport,
|
|
151
154
|
onZoomViewport,
|
|
152
155
|
onSetViewport,
|
|
@@ -171,7 +174,7 @@ export function CompositePanelSurface({
|
|
|
171
174
|
const activeDrag = toolDrag ?? keyboardDrag?.drag ?? null;
|
|
172
175
|
// A keyboard placement's end sits at the level the user put it, so it
|
|
173
176
|
// draws a level line where otherwise only the pointer would.
|
|
174
|
-
const heldCursorYRatio = keyboardDrag ? keyboardDrag.drag.endYRatio : cursorYRatio;
|
|
177
|
+
const heldCursorYRatio = keyboardDrag ? keyboardDrag.drag.endYRatio : sharedCursor ? null : cursorYRatio;
|
|
175
178
|
const seriesCursorYRatio = useMemo(
|
|
176
179
|
() => resolveSeriesCursorYRatio(panel, scene),
|
|
177
180
|
[panel, scene],
|
|
@@ -207,7 +210,11 @@ export function CompositePanelSurface({
|
|
|
207
210
|
// The level line follows the pointer only. A keyboard or shared cursor knows
|
|
208
211
|
// its column, and the series markers and axis readout already say the value.
|
|
209
212
|
// A tool placed from the keyboard is the exception: its end is a level.
|
|
210
|
-
|
|
213
|
+
// On the desktop and the web an ordinary hover reads each series on its own
|
|
214
|
+
// axis and draws only the vertical line; the level line, and the pointer's
|
|
215
|
+
// value on the axes, belong to an armed or placing tool.
|
|
216
|
+
const readsPointerLevel = !isDesktopWeb || armedTool !== null || activeDrag !== null;
|
|
217
|
+
const pointerCursorYRatio = scene.cursorXRatio === null || !readsPointerLevel ? null : heldCursorYRatio;
|
|
211
218
|
const crosshair = useMemo(
|
|
212
219
|
() => resolvePanelCrosshair(panel, bitmap, scene, pointerCursorYRatio, colors.crosshair),
|
|
213
220
|
[bitmap, colors.crosshair, panel, pointerCursorYRatio, scene],
|
|
@@ -293,6 +300,10 @@ export function CompositePanelSurface({
|
|
|
293
300
|
() => levels && levelDomain ? projectLevels(levels.items, levelDomain, levelDraft) : NO_LEVELS,
|
|
294
301
|
[levelDomain, levelDraft, levels],
|
|
295
302
|
);
|
|
303
|
+
// Drawings project through the time scale alone: the crosshair moving,
|
|
304
|
+
// here or on a linked chart, must not repaint the plot, which the terminal's
|
|
305
|
+
// kitty surface would upload whole again.
|
|
306
|
+
const drawingProjection = useMemo(() => ({ timeScale: scene.timeScale }), [scene.timeScale]);
|
|
296
307
|
const bitmapLayers = useMemo(() => {
|
|
297
308
|
if (!bitmap) return null;
|
|
298
309
|
// The desktop composites overlays as vectors, so the plot raster stays put
|
|
@@ -309,7 +320,7 @@ export function CompositePanelSurface({
|
|
|
309
320
|
draw: drawColor,
|
|
310
321
|
},
|
|
311
322
|
toolReadout?.direction ?? "up",
|
|
312
|
-
{ scene, panel, items: panelDrawings, selectedId: selectedDrawingId },
|
|
323
|
+
{ scene: drawingProjection, panel, items: panelDrawings, selectedId: selectedDrawingId },
|
|
313
324
|
);
|
|
314
325
|
return [projectedLevels.length > 0 ? paintLevels(base, projectedLevels, selectedLevelId) : base];
|
|
315
326
|
}, [
|
|
@@ -318,11 +329,11 @@ export function CompositePanelSurface({
|
|
|
318
329
|
colors.crosshair,
|
|
319
330
|
colors.negative,
|
|
320
331
|
drawColor,
|
|
332
|
+
drawingProjection,
|
|
321
333
|
isDesktopWeb,
|
|
322
334
|
panel,
|
|
323
335
|
panelDrawings,
|
|
324
336
|
projectedLevels,
|
|
325
|
-
scene,
|
|
326
337
|
selectedDrawingId,
|
|
327
338
|
selectedLevelId,
|
|
328
339
|
toolReadout?.direction,
|
|
@@ -394,8 +405,11 @@ export function CompositePanelSurface({
|
|
|
394
405
|
const cursorPixelY = activeCursorYRatio === null
|
|
395
406
|
? null
|
|
396
407
|
: activeCursorYRatio * Math.max(panel.height * cellHeightPx - 1, 0);
|
|
397
|
-
|
|
398
|
-
const
|
|
408
|
+
// The pointer's value on the axes follows the level line it belongs to.
|
|
409
|
+
const axisCursorYRatio = isDesktopWeb ? pointerCursorYRatio : activeCursorYRatio;
|
|
410
|
+
const leftCursorLabel = cursorAxisLabel(panel, "left", axisCursorYRatio, formatAxisValue);
|
|
411
|
+
const rightCursorLabel = cursorAxisLabel(panel, "right", axisCursorYRatio, formatAxisValue);
|
|
412
|
+
const seriesAxisTags = useMemo(() => isDesktopWeb ? resolveSeriesAxisTags(panel, scene, formatAxisValue) : [], [formatAxisValue, isDesktopWeb, panel, scene]);
|
|
399
413
|
const measureReadout = useMemo(() => {
|
|
400
414
|
if (!activeDrag || !toolReadout?.summary) return null;
|
|
401
415
|
const text = toolReadout.summary;
|
|
@@ -846,6 +860,8 @@ export function CompositePanelSurface({
|
|
|
846
860
|
cursorBackgroundColor={colors.background}
|
|
847
861
|
axisColor={colors.textDim}
|
|
848
862
|
extraMarkers={leftAxisMarkers}
|
|
863
|
+
seriesTags={isDesktopWeb ? seriesAxisTags.filter((tag) => tag.side === "left") : undefined}
|
|
864
|
+
measuredPixelHeight={plotSize.height * cellHeightPx}
|
|
849
865
|
/>
|
|
850
866
|
<Box width={axisGap} />
|
|
851
867
|
</>
|
|
@@ -893,6 +909,8 @@ export function CompositePanelSurface({
|
|
|
893
909
|
cursorBackgroundColor={colors.background}
|
|
894
910
|
axisColor={colors.textDim}
|
|
895
911
|
extraMarkers={rightAxisMarkers}
|
|
912
|
+
seriesTags={isDesktopWeb ? seriesAxisTags.filter((tag) => tag.side === "right") : undefined}
|
|
913
|
+
measuredPixelHeight={plotSize.height * cellHeightPx}
|
|
896
914
|
/>
|
|
897
915
|
</>
|
|
898
916
|
) : null}
|
|
@@ -2,6 +2,8 @@ import { useMemo } from "react";
|
|
|
2
2
|
import { Box, Text, useUiCapabilities } from "../../../ui";
|
|
3
3
|
import { colors } from "../../../theme/colors";
|
|
4
4
|
import { clamp } from "../../../utils/math";
|
|
5
|
+
import type { SeriesAxisTag } from "./panel-labels";
|
|
6
|
+
import { placeSeriesAxisTags, SeriesAxisTags } from "./series-axis-tags";
|
|
5
7
|
|
|
6
8
|
function formatAxisCell(label: string | null, width: number): string {
|
|
7
9
|
if (width <= 0) return "";
|
|
@@ -34,6 +36,9 @@ interface PriceAxisLabelsProps {
|
|
|
34
36
|
axisColor?: string;
|
|
35
37
|
/** Anchors that stay put while the cursor moves, such as a measure start. */
|
|
36
38
|
extraMarkers?: readonly PriceAxisMarker[];
|
|
39
|
+
/** Desktop/web observations, separate from a drawing tool's endpoint. */
|
|
40
|
+
seriesTags?: readonly SeriesAxisTag[];
|
|
41
|
+
measuredPixelHeight?: number;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
interface CursorPriceAxisOverlay {
|
|
@@ -134,6 +139,8 @@ export function PriceAxisLabels({
|
|
|
134
139
|
cursorBackgroundColor = colors.bg,
|
|
135
140
|
axisColor = colors.textDim,
|
|
136
141
|
extraMarkers,
|
|
142
|
+
seriesTags,
|
|
143
|
+
measuredPixelHeight,
|
|
137
144
|
}: PriceAxisLabelsProps) {
|
|
138
145
|
const { cellHeightPx = 18, fractionalViewport = false } = useUiCapabilities();
|
|
139
146
|
const overlay = useMemo(() => buildCursorPriceAxisOverlay({
|
|
@@ -147,7 +154,11 @@ export function PriceAxisLabels({
|
|
|
147
154
|
const usePixelOverlay = fractionalViewport && overlay.labelText !== null && overlay.topPercent !== null;
|
|
148
155
|
// Row-snapped labels drift up to a row off the gridlines they name.
|
|
149
156
|
const usePixelTicks = fractionalViewport && axisTicks !== undefined;
|
|
150
|
-
const pixelHeight = Math.max(height * cellHeightPx, 1);
|
|
157
|
+
const pixelHeight = Math.max(measuredPixelHeight ?? height * cellHeightPx, 1);
|
|
158
|
+
const placedSeriesTags = seriesTags ? placeSeriesAxisTags(seriesTags, pixelHeight, cellHeightPx) : [];
|
|
159
|
+
const lanes = Math.max(1, ...placedSeriesTags.map((tag) => tag.lane + 1));
|
|
160
|
+
// Fixed reference labels yield to observations, never the other way round.
|
|
161
|
+
const visibleMarkers = extraMarkers?.filter((marker) => placedSeriesTags.every((tag) => Math.abs(tag.centerY - marker.pixelY) >= cellHeightPx));
|
|
151
162
|
const placedTicks = usePixelTicks
|
|
152
163
|
? placeAxisTicks({
|
|
153
164
|
ticks: axisTicks,
|
|
@@ -155,7 +166,8 @@ export function PriceAxisLabels({
|
|
|
155
166
|
cellHeightPx,
|
|
156
167
|
badgeCentersPx: [
|
|
157
168
|
...(usePixelOverlay ? [(overlay.topPercent! / 100) * (pixelHeight - 1)] : []),
|
|
158
|
-
...(
|
|
169
|
+
...(visibleMarkers?.map((marker) => marker.pixelY) ?? []),
|
|
170
|
+
...placedSeriesTags.map((tag) => tag.centerY),
|
|
159
171
|
],
|
|
160
172
|
})
|
|
161
173
|
: null;
|
|
@@ -233,7 +245,7 @@ export function PriceAxisLabels({
|
|
|
233
245
|
</Box>
|
|
234
246
|
);
|
|
235
247
|
}) : null}
|
|
236
|
-
{usePixelOverlay || usePixelTicks ?
|
|
248
|
+
{usePixelOverlay || usePixelTicks ? visibleMarkers?.map((marker) => (
|
|
237
249
|
<Box
|
|
238
250
|
key={`${marker.label}:${marker.pixelY}`}
|
|
239
251
|
width={axisSectionWidth}
|
|
@@ -255,6 +267,7 @@ export function PriceAxisLabels({
|
|
|
255
267
|
{axisPaddingWidth > 0 ? <Box width={axisPaddingWidth} /> : null}
|
|
256
268
|
</Box>
|
|
257
269
|
)) : null}
|
|
270
|
+
{seriesTags ? <SeriesAxisTags placed={placedSeriesTags} pixelHeight={pixelHeight} lanes={lanes} background={cursorBackgroundColor} /> : null}
|
|
258
271
|
{usePixelOverlay ? (
|
|
259
272
|
<Box
|
|
260
273
|
width={axisSectionWidth}
|
|
@@ -661,16 +661,73 @@ function buildCursorValues(
|
|
|
661
661
|
}));
|
|
662
662
|
}
|
|
663
663
|
|
|
664
|
+
/**
|
|
665
|
+
* The bar a time from another chart lands on: the last one at or before it,
|
|
666
|
+
* as a bar stands for the time from its open until the next. A time before
|
|
667
|
+
* the first bar in view lands on none, and so does one past the plot's right
|
|
668
|
+
* edge while the chart is panned back from its newest bar, rather than
|
|
669
|
+
* pinning the cursor to an edge the time is not at. A chart showing its
|
|
670
|
+
* newest bar lands any later time on it, so a monthly series still reads its
|
|
671
|
+
* latest month on a day after it.
|
|
672
|
+
*/
|
|
673
|
+
function linkedCursorDate(scene: CompositeChartScene, requested: Date): Date | null {
|
|
674
|
+
const target = requested.getTime();
|
|
675
|
+
const { dates, timeScale } = scene;
|
|
676
|
+
if (!Number.isFinite(target) || dates.length === 0) return null;
|
|
677
|
+
const plotEnd = unprojectCompositeTimestamp(timeScale, 1);
|
|
678
|
+
const newest = timeScale.kind === "market" ? timeScale.anchors.at(-1)?.timestamp : scene.newestTime;
|
|
679
|
+
if (target > plotEnd && (newest === undefined || newest > plotEnd)) return null;
|
|
680
|
+
let low = 0;
|
|
681
|
+
let high = dates.length;
|
|
682
|
+
while (low < high) {
|
|
683
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
684
|
+
if (dates[middle]!.getTime() <= target) low = middle + 1;
|
|
685
|
+
else high = middle;
|
|
686
|
+
}
|
|
687
|
+
return dates[low - 1] ?? null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** The bar another chart's cursor time lands on (see `linkedCursorDate`), as its timestamp. */
|
|
691
|
+
export function compositeLinkedCursorTime(scene: CompositeChartScene, time: number): number | null {
|
|
692
|
+
return linkedCursorDate(scene, new Date(time))?.getTime() ?? null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The last instant of the bar a cursor is on, which is the time it is shared
|
|
697
|
+
* at: a bar stands until the next one or its own interval ends, whichever is
|
|
698
|
+
* first. Another chart then lands on its last bar at or before it, so a daily
|
|
699
|
+
* cursor shows the day's last intraday bar, whose close is the day's, and an
|
|
700
|
+
* intraday cursor shows its own day's bar rather than the next one's. On
|
|
701
|
+
* calendar time the newest bar, with no next one, stands as long as the one
|
|
702
|
+
* before it did, so a series' latest month lands a daily chart on that
|
|
703
|
+
* month's latest day rather than its first.
|
|
704
|
+
*/
|
|
705
|
+
export function compositeCursorAsOf(scene: CompositeChartScene, date: Date): number {
|
|
706
|
+
const start = date.getTime();
|
|
707
|
+
const next = scene.dates.find((entry) => entry.getTime() > start)?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
708
|
+
const previous = scene.dates.findLast((entry) => entry.getTime() < start)?.getTime();
|
|
709
|
+
const intervalEnd = scene.timeScale.kind === "market" ? start + scene.timeScale.cadenceMs
|
|
710
|
+
: next === Number.POSITIVE_INFINITY && previous !== undefined ? start + (start - previous)
|
|
711
|
+
: Number.POSITIVE_INFINITY;
|
|
712
|
+
const end = Math.min(next, intervalEnd);
|
|
713
|
+
return Number.isFinite(end) ? end - 1 : start;
|
|
714
|
+
}
|
|
715
|
+
|
|
664
716
|
/**
|
|
665
717
|
* Applies cursor-only state without rebuilding panel domains or projected
|
|
666
718
|
* series. Keeping those references stable lets renderers reuse the expensive
|
|
667
|
-
* base chart while the cursor moves.
|
|
719
|
+
* base chart while the cursor moves. `linked` places a time another chart's
|
|
720
|
+
* cursor is at (see `linkedCursorDate`); otherwise the cursor goes to the
|
|
721
|
+
* nearest bar.
|
|
668
722
|
*/
|
|
669
723
|
export function applyCompositeChartCursor(
|
|
670
724
|
scene: CompositeChartScene,
|
|
671
725
|
requestedCursor: Date | null,
|
|
726
|
+
snap: "nearest" | "linked" = "nearest",
|
|
672
727
|
): CompositeChartScene {
|
|
673
|
-
const cursorDate = requestedCursor ?
|
|
728
|
+
const cursorDate = !requestedCursor ? null
|
|
729
|
+
: snap === "linked" ? linkedCursorDate(scene, requestedCursor)
|
|
730
|
+
: nearestDate(scene.dates, requestedCursor);
|
|
674
731
|
const currentTimestamp = scene.cursorDate?.getTime() ?? null;
|
|
675
732
|
const nextTimestamp = cursorDate?.getTime() ?? null;
|
|
676
733
|
if (currentTimestamp === nextTimestamp) return scene;
|
|
@@ -818,6 +875,7 @@ export function buildCompositeChartScene(
|
|
|
818
875
|
cursorDate,
|
|
819
876
|
cursorXRatio,
|
|
820
877
|
cursorValues: buildCursorValues(panelScenes, cursorDate, { startTime, endTime, timeScale }),
|
|
878
|
+
newestTime: lastTime,
|
|
821
879
|
...(timeZone ? { timeZone } : {}),
|
|
822
880
|
};
|
|
823
881
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Box, Text } from "../../../ui";
|
|
2
|
+
import { clamp } from "../../../utils/math";
|
|
3
|
+
import type { SeriesAxisTag } from "./panel-labels";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Keep every tag. Order is by value height, then authored series order for
|
|
7
|
+
* ties. A forward pass separates neighbours; a backward pass fits the bottom
|
|
8
|
+
* edge. Panels shorter than the readout use parallel lanes, not dropped tags
|
|
9
|
+
* or smaller type. The gutter reserves those lanes before the cursor moves.
|
|
10
|
+
*/
|
|
11
|
+
export function placeSeriesAxisTags(tags: readonly SeriesAxisTag[], pixelHeight: number, labelHeight: number) {
|
|
12
|
+
const height = Math.max(pixelHeight, labelHeight);
|
|
13
|
+
const capacity = Math.max(1, Math.floor(height / labelHeight));
|
|
14
|
+
const sorted = tags.map((tag, order) => ({ ...tag, order, anchorY: clamp(tag.yRatio, 0, 1) * Math.max(pixelHeight - 1, 0) }))
|
|
15
|
+
.sort((a, b) => a.anchorY - b.anchorY || a.order - b.order);
|
|
16
|
+
const placed: Array<SeriesAxisTag & { anchorY: number; centerY: number; lane: number }> = [];
|
|
17
|
+
for (let start = 0; start < sorted.length; start += capacity) {
|
|
18
|
+
const lane = sorted.slice(start, start + capacity);
|
|
19
|
+
const centers = lane.map((tag) => clamp(tag.anchorY, labelHeight / 2, height - labelHeight / 2));
|
|
20
|
+
for (let i = 1; i < centers.length; i++) centers[i] = Math.max(centers[i]!, centers[i - 1]! + labelHeight);
|
|
21
|
+
if (centers.length) centers[centers.length - 1] = Math.min(centers.at(-1)!, height - labelHeight / 2);
|
|
22
|
+
for (let i = centers.length - 2; i >= 0; i--) centers[i] = Math.min(centers[i]!, centers[i + 1]! - labelHeight);
|
|
23
|
+
lane.forEach((tag, index) => placed.push({ ...tag, centerY: centers[index]!, lane: Math.floor(start / capacity) }));
|
|
24
|
+
}
|
|
25
|
+
return placed;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function SeriesAxisTags({ placed, pixelHeight, lanes, background }: {
|
|
29
|
+
placed: ReturnType<typeof placeSeriesAxisTags>;
|
|
30
|
+
pixelHeight: number;
|
|
31
|
+
lanes: number;
|
|
32
|
+
background: string;
|
|
33
|
+
}) {
|
|
34
|
+
const percent = (y: number) => `${100 * y / Math.max(pixelHeight, 1)}%`;
|
|
35
|
+
return placed.map((tag) => {
|
|
36
|
+
const left = tag.side === "left";
|
|
37
|
+
const edge = left ? "right" : "left";
|
|
38
|
+
const inset = `${tag.lane * 100 / lanes}%`;
|
|
39
|
+
return <Box key={tag.seriesId} style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
|
|
40
|
+
{/* The tick stays at the observation, even when the badge must move. */}
|
|
41
|
+
<Box
|
|
42
|
+
data-gloom-role="chart-series-axis-anchor"
|
|
43
|
+
data-series-id={tag.seriesId}
|
|
44
|
+
style={{ position: "absolute", [edge]: 0, top: percent(tag.anchorY), width: `calc(${inset} + .5em)`, height: 0, borderTop: `1px ${tag.carried ? "dashed" : "solid"} ${tag.color}` }}
|
|
45
|
+
/>
|
|
46
|
+
<Box style={{ position: "absolute", [edge]: `calc(${inset} + .25em)`, top: percent(Math.min(tag.anchorY, tag.centerY)), height: percent(Math.abs(tag.centerY - tag.anchorY)), width: 0, borderLeft: `1px solid ${tag.color}` }} />
|
|
47
|
+
<Box
|
|
48
|
+
data-gloom-role="chart-series-axis-tag"
|
|
49
|
+
data-series-id={tag.seriesId}
|
|
50
|
+
data-side={tag.side}
|
|
51
|
+
data-value={tag.value}
|
|
52
|
+
data-carried={tag.carried ? "true" : undefined}
|
|
53
|
+
aria-label={`${tag.seriesLabel}: ${tag.label}${tag.carried ? " (carried)" : ""}`}
|
|
54
|
+
style={{
|
|
55
|
+
position: "absolute", [edge]: `calc(${inset} + .5em)`, top: percent(tag.centerY), transform: "translateY(-50%)",
|
|
56
|
+
width: `calc(${100 / lanes}% - .5em)`, height: "1.5em", lineHeight: "1.5em",
|
|
57
|
+
alignItems: left ? "flex-end" : "flex-start", justifyContent: "center",
|
|
58
|
+
// Every badge paints above every leader, including another lane's.
|
|
59
|
+
zIndex: 2, color: tag.color, backgroundColor: background, whiteSpace: "nowrap",
|
|
60
|
+
borderBlockEnd: tag.carried ? `1px dashed ${tag.color}` : undefined,
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Text fg={tag.color} selectable={false}>{tag.label}</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
</Box>;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -153,7 +153,7 @@ export function resolveDrawingFromDrag(
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function projectDrawing(
|
|
156
|
-
scene: CompositeChartScene,
|
|
156
|
+
scene: Pick<CompositeChartScene, "timeScale">,
|
|
157
157
|
panel: CompositePanelScene,
|
|
158
158
|
drawing: ChartDrawing,
|
|
159
159
|
): Array<{ xRatio: number; yRatio: number }> | null {
|
|
@@ -516,7 +516,7 @@ export function drawChartToolOverlay(
|
|
|
516
516
|
colors: ChartToolColors,
|
|
517
517
|
direction: "up" | "down" = "up",
|
|
518
518
|
drawings?: {
|
|
519
|
-
scene: CompositeChartScene
|
|
519
|
+
scene: Pick<CompositeChartScene, "timeScale">;
|
|
520
520
|
panel: CompositePanelScene;
|
|
521
521
|
items: readonly ChartDrawing[];
|
|
522
522
|
selectedId?: string | null;
|
|
@@ -181,6 +181,8 @@ export interface CompositeChartScene {
|
|
|
181
181
|
cursorPanels?: CompositePanelScene[];
|
|
182
182
|
/** IANA zone the time axis and cursor read in: the venue's for intraday market bars, else UTC. */
|
|
183
183
|
timeZone?: string;
|
|
184
|
+
/** The newest observation's time, in view or not. */
|
|
185
|
+
newestTime?: number;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
export interface BuildCompositeChartSceneOptions {
|
|
@@ -222,6 +224,27 @@ export interface CompositeChartXAxis {
|
|
|
222
224
|
formatCursor?: (xRatio: number) => string;
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
/**
|
|
228
|
+
* A crosshair shared between charts, as the last instant of the bar it is
|
|
229
|
+
* on. The chart subscribes itself, so a move re-renders the charts and not
|
|
230
|
+
* the panes around them.
|
|
231
|
+
*/
|
|
232
|
+
interface CompositeCursorLink {
|
|
233
|
+
subscribe(listener: () => void): () => void;
|
|
234
|
+
/**
|
|
235
|
+
* Another chart's cursor, shown in place of this chart's own: on the last
|
|
236
|
+
* bar at or before it, and not at all outside the view. Null while this
|
|
237
|
+
* chart's cursor is the shared one, or there is none.
|
|
238
|
+
*/
|
|
239
|
+
time(): number | null;
|
|
240
|
+
/** Whether this chart's cursor is the shared one. */
|
|
241
|
+
held(): boolean;
|
|
242
|
+
/** Reports this chart's cursor, or null when it leaves. */
|
|
243
|
+
publish(time: number | null): void;
|
|
244
|
+
/** Clears the shared cursor wherever it is, as Esc on a chart showing it does. */
|
|
245
|
+
clear(): void;
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
export interface CompositeChartProps {
|
|
226
249
|
series: ResolvedSeries[];
|
|
227
250
|
/** Optional legend model; can include hidden series that are not plotted. */
|
|
@@ -248,6 +271,11 @@ export interface CompositeChartProps {
|
|
|
248
271
|
/** Permit panning into an older unloaded window so the owner can backfill it. */
|
|
249
272
|
allowHistoricalBackfill?: boolean;
|
|
250
273
|
cursorDate?: Date | null;
|
|
274
|
+
/**
|
|
275
|
+
* A crosshair shared with other charts. By default an interactive chart
|
|
276
|
+
* with a time axis in a pane shares its layout tab's; null keeps it out.
|
|
277
|
+
*/
|
|
278
|
+
cursorLink?: CompositeCursorLink | null;
|
|
251
279
|
viewport?: {
|
|
252
280
|
start: Date;
|
|
253
281
|
end: Date;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { useOptionalPaneInstanceId } from "../../../state/app/context";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A pane's range control and its charts share one request: choosing the range
|
|
6
|
+
* that is already selected sends the charts back to it, as `0` does on a chart
|
|
7
|
+
* whose view was panned or zoomed. A chart answers for its own view, and a pane
|
|
8
|
+
* that keeps a view of its own (a saved one, a loaded window) answers for that.
|
|
9
|
+
*/
|
|
10
|
+
type ViewReset = () => void;
|
|
11
|
+
|
|
12
|
+
const listeners = new Map<string, Set<ViewReset>>();
|
|
13
|
+
/** Charts outside a pane (a dialog, a test) share one scope. */
|
|
14
|
+
const NO_PANE = "";
|
|
15
|
+
|
|
16
|
+
function runViewResets(scope: string): void {
|
|
17
|
+
for (const reset of [...listeners.get(scope) ?? []]) reset();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Registers what happens in this pane when a range is chosen again. Keep it
|
|
22
|
+
* idempotent and a no-op for a view that was never moved: the request also
|
|
23
|
+
* arrives when the user presses the range that was already showing.
|
|
24
|
+
*/
|
|
25
|
+
export function useChartViewResetListener(reset: ViewReset, enabled = true): void {
|
|
26
|
+
const scope = useOptionalPaneInstanceId() ?? NO_PANE;
|
|
27
|
+
const latest = useRef(reset);
|
|
28
|
+
latest.current = reset;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!enabled) return;
|
|
31
|
+
const run = () => latest.current();
|
|
32
|
+
let set = listeners.get(scope);
|
|
33
|
+
if (!set) listeners.set(scope, set = new Set());
|
|
34
|
+
set.add(run);
|
|
35
|
+
return () => {
|
|
36
|
+
set.delete(run);
|
|
37
|
+
if (set.size === 0 && listeners.get(scope) === set) listeners.delete(scope);
|
|
38
|
+
};
|
|
39
|
+
}, [enabled, scope]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Sends this pane's charts back to the range they were given. */
|
|
43
|
+
export function useChartViewReset(): () => void {
|
|
44
|
+
const scope = useOptionalPaneInstanceId() ?? NO_PANE;
|
|
45
|
+
return useCallback(() => runViewResets(scope), [scope]);
|
|
46
|
+
}
|