gloomberb 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +2 -1
- package/src/api-client/beneficial-owners.ts +92 -0
- package/src/api-client/data.ts +44 -11
- package/src/api-client/errors.ts +12 -0
- package/src/api-client/geo.ts +363 -0
- package/src/api-client/index.ts +5 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/members.ts +26 -0
- package/src/api-client/paths.ts +30 -1
- package/src/api-client/perps.ts +41 -0
- package/src/api-client/request.ts +5 -2
- package/src/api-client/types.ts +9 -1
- package/src/app/global-shortcuts.ts +14 -6
- package/src/app/keybindings/actions.ts +1 -1
- package/src/app/runtime/update.ts +38 -2
- package/src/app.tsx +10 -6
- package/src/brokers/account-cache.ts +24 -0
- package/src/cli/commands/command-utils.ts +8 -0
- package/src/cli/commands/crypto-hints.ts +75 -0
- package/src/cli/commands/market.ts +414 -106
- package/src/cli/commands/overview.ts +77 -32
- package/src/cli/commands/ticker.ts +101 -43
- package/src/cli/desktop-pane-shot.ts +34 -4
- package/src/cli/freshness.ts +61 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/helpers.ts +4 -8
- package/src/cli/index.ts +12 -2
- package/src/cli/listing-arg.ts +321 -0
- package/src/cli/pane-functions/access-gate.ts +42 -0
- package/src/cli/pane-functions/catalog.ts +28 -1
- package/src/cli/pane-functions/data.ts +3 -1
- package/src/cli/pane-functions/dom.ts +20 -2
- package/src/cli/pane-functions/freshness.ts +400 -0
- package/src/cli/pane-functions/function-help.ts +36 -22
- package/src/cli/pane-functions/headless.ts +39 -11
- package/src/cli/pane-functions/index.ts +15 -4
- package/src/cli/pane-functions/options.ts +3 -0
- package/src/cli/pane-functions/report.ts +4 -0
- package/src/cli/pane-functions/resolver.ts +35 -7
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +7 -3
- package/src/cli/result.ts +41 -27
- package/src/cli/ticker-resolution.ts +7 -5
- package/src/components/chart/composite/composite-chart.tsx +88 -2536
- package/src/components/chart/composite/drawing-store.ts +59 -0
- package/src/components/chart/composite/legend.tsx +319 -0
- package/src/components/chart/composite/panel-bitmap.ts +139 -0
- package/src/components/chart/composite/panel-gestures.ts +59 -0
- package/src/components/chart/composite/panel-labels.ts +107 -0
- package/src/components/chart/composite/panel-surface.tsx +876 -0
- package/src/components/chart/composite/tool-catalog.ts +98 -0
- package/src/components/chart/composite/toolbar.tsx +186 -0
- package/src/components/chart/composite/tools.ts +8 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +703 -0
- package/src/components/chart/composite/use-composite-viewport.ts +143 -0
- package/src/components/command-bar/commands/collection.ts +5 -1
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/list/view.tsx +16 -3
- package/src/components/command-bar/pane-templates/workflow.ts +1 -0
- package/src/components/command-bar/routes/root/command-items.ts +9 -2
- package/src/components/command-bar/routes/root/plugin-install.ts +2 -2
- package/src/components/command-bar/routes/root/provider-search.ts +19 -5
- package/src/components/command-bar/routes/root/runtime.ts +14 -5
- package/src/components/command-bar/routes/ticker-search/actions.ts +5 -0
- package/src/components/command-bar/routes/ticker-search/results.ts +31 -5
- package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
- package/src/components/command-bar/routing/actions.ts +2 -0
- package/src/components/command-bar/selection-runtime.ts +38 -0
- package/src/components/command-bar/surface/index.tsx +7 -2
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/command-bar/workflow/tickers.ts +6 -0
- package/src/components/data-table/view.tsx +29 -10
- package/src/components/feedback-dialog.tsx +2 -1
- package/src/components/form-modal/field-row.tsx +2 -2
- package/src/components/index.ts +2 -2
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +6 -0
- package/src/components/layout/header.tsx +24 -7
- package/src/components/layout/missing-pane.tsx +2 -2
- package/src/components/layout/pane/content.tsx +23 -10
- package/src/components/layout/pane/error-boundary.tsx +103 -0
- package/src/components/layout/pane/footer/registration.tsx +68 -23
- package/src/components/layout/pane/header.tsx +61 -28
- package/src/components/layout/pane/index.tsx +6 -0
- package/src/components/layout/pane/title.ts +31 -7
- package/src/components/layout/shell/drag/index.ts +6 -5
- package/src/components/layout/shell/drag/runtime.ts +3 -0
- package/src/components/layout/shell/index.tsx +6 -0
- package/src/components/layout/shell/menu.ts +2 -1
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +29 -10
- package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -3
- package/src/components/layout/shell/ticker-link-menu.ts +23 -9
- package/src/components/metric-treemap/heat-surface.tsx +745 -0
- package/src/components/metric-treemap/heat-tiles.ts +228 -0
- package/src/components/metric-treemap/index.tsx +2 -0
- package/src/components/metric-treemap/layout.ts +8 -0
- package/src/components/metric-treemap/scene.ts +432 -0
- package/src/components/onboarding/onboarding-keyboard.ts +311 -0
- package/src/components/onboarding/onboarding-wizard.tsx +101 -788
- package/src/components/onboarding/use-onboarding-broker-form.ts +263 -0
- package/src/components/onboarding/use-onboarding-progress.ts +298 -0
- package/src/components/onboarding/use-onboarding-upgrade.ts +83 -0
- package/src/components/pane-settings-dialog/field-dialogs.tsx +1 -0
- package/src/components/ticker/list-table-view.tsx +13 -1
- package/src/components/toggle-list.tsx +38 -12
- package/src/components/ui/country-flag-art.tsx +449 -0
- package/src/components/ui/country-flag.tsx +41 -0
- package/src/components/ui/data-table/types.ts +13 -0
- package/src/components/ui/fields.tsx +8 -0
- package/src/components/ui/icon.tsx +2 -0
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/multi-select/dialog.tsx +108 -22
- package/src/components/ui/range-track.tsx +53 -0
- package/src/components/ui/status.tsx +8 -2
- package/src/components/ui/tabs.tsx +11 -4
- package/src/components/ui/text-prompt-dialog.tsx +4 -2
- package/src/core/state/app/types.ts +10 -1
- package/src/data/config/store/normalize.ts +17 -1
- package/src/i18n/es.ts +24 -4
- package/src/i18n/ja.ts +24 -4
- package/src/i18n/ko.ts +24 -4
- package/src/i18n/zh-cn.ts +24 -4
- package/src/i18n/zh-tw.ts +24 -4
- package/src/layout/ticker-navigation.ts +28 -2
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/quotes.ts +23 -4
- package/src/market-data/fx-closes.ts +72 -0
- package/src/market-data/market/freshness.ts +78 -6
- package/src/market-data/market/session-close-note.ts +57 -0
- package/src/market-data/market/status.ts +151 -0
- package/src/market-data/published-apac-sessions.ts +66 -0
- package/src/market-data/quotes/contributions.ts +8 -0
- package/src/market-data/quotes/freshness.ts +36 -0
- package/src/market-data/quotes/resolution.ts +24 -8
- package/src/market-data/quotes/screener-live-quotes.ts +49 -4
- package/src/market-data/quotes/time.ts +16 -0
- package/src/plugins/builtin/account-management/pane.tsx +4 -0
- package/src/plugins/builtin/alerts/cli.ts +10 -5
- package/src/plugins/builtin/alerts/index.tsx +1 -0
- package/src/plugins/builtin/analytics/metrics.ts +8 -3
- package/src/plugins/builtin/analytics/risk-client.ts +330 -79
- package/src/plugins/builtin/analytics/risk-fx.ts +102 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +61 -49
- package/src/plugins/builtin/analytics/risk-math.ts +33 -13
- package/src/plugins/builtin/analytics/risk-model.ts +282 -64
- package/src/plugins/builtin/analytics/risk-pane.tsx +62 -30
- package/src/plugins/builtin/analytics/risk-test-data.ts +206 -0
- package/src/plugins/builtin/attention/headless.ts +6 -4
- package/src/plugins/builtin/attention/index.tsx +1 -1
- package/src/plugins/builtin/attention/pane.tsx +1 -1
- package/src/plugins/builtin/awards/headless.ts +1 -0
- package/src/plugins/builtin/awards/pane.tsx +1 -1
- package/src/plugins/builtin/backtest/headless.ts +1 -0
- package/src/plugins/builtin/bond-calculator/headless.ts +1 -0
- package/src/plugins/builtin/buildout/cells.tsx +15 -12
- package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
- package/src/plugins/builtin/buildout/detail/index.tsx +0 -1
- package/src/plugins/builtin/buildout/detail/intel.tsx +6 -6
- package/src/plugins/builtin/buildout/detail/site.tsx +4 -7
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -19
- package/src/plugins/builtin/buildout/index.tsx +1 -0
- package/src/plugins/builtin/catalysts/headless.ts +3 -1
- package/src/plugins/builtin/catalysts/index.tsx +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -1
- package/src/plugins/builtin/cds/headless.ts +2 -0
- package/src/plugins/builtin/cds/index.tsx +1 -0
- package/src/plugins/builtin/central-bank-rates/headless.ts +1 -0
- package/src/plugins/builtin/chart-composer/headless.ts +38 -3
- package/src/plugins/builtin/chart-composer/index.tsx +3 -1
- package/src/plugins/builtin/chart-composer/series-expression.ts +18 -2
- package/src/plugins/builtin/chat/channels.ts +19 -3
- package/src/plugins/builtin/chat/content/composer-runtime.ts +28 -13
- package/src/plugins/builtin/chat/content/footer.tsx +15 -6
- package/src/plugins/builtin/chat/content/index.tsx +120 -21
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +261 -113
- package/src/plugins/builtin/chat/content/transcript.tsx +20 -2
- package/src/plugins/builtin/chat/direct-messages.ts +200 -0
- package/src/plugins/builtin/chat/index.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -0
- package/src/plugins/builtin/chat/message/header.tsx +16 -6
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +72 -32
- package/src/plugins/builtin/chat/message/profile-popover.tsx +96 -12
- package/src/plugins/builtin/chat/message/terminal.tsx +3 -0
- package/src/plugins/builtin/chat/message/types.ts +14 -2
- package/src/plugins/builtin/chat/profile-popover.ts +18 -13
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +2 -1
- package/src/plugins/builtin/cloud/plugin.tsx +1 -0
- package/src/plugins/builtin/cloud/team/module.tsx +1 -0
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -0
- package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
- package/src/plugins/builtin/comment-letters/index.tsx +1 -0
- package/src/plugins/builtin/company-attention/headless.ts +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/company-kpis/headless.ts +1 -0
- package/src/plugins/builtin/congress-trades/headless.ts +1 -0
- package/src/plugins/builtin/correlation/compute.ts +59 -16
- package/src/plugins/builtin/correlation/geo.ts +34 -0
- package/src/plugins/builtin/correlation/headless-schema.ts +4 -1
- package/src/plugins/builtin/correlation/headless.ts +42 -8
- package/src/plugins/builtin/correlation/history.ts +2 -0
- package/src/plugins/builtin/correlation/index.tsx +68 -14
- package/src/plugins/builtin/correlation/matrix/model.ts +17 -2
- package/src/plugins/builtin/correlation/matrix/selection.ts +5 -3
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +4 -2
- package/src/plugins/builtin/correlation/relationship/view-model.ts +16 -1
- package/src/plugins/builtin/correlation/settings.ts +1 -1
- package/src/plugins/builtin/cot/headless.ts +6 -3
- package/src/plugins/builtin/cot/index.tsx +3 -3
- package/src/plugins/builtin/cot/model.ts +36 -3
- package/src/plugins/builtin/cpi/headless.ts +4 -0
- package/src/plugins/builtin/credit-boards/headless.ts +2 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +1 -0
- package/src/plugins/builtin/credit-documents/gauge.tsx +5 -8
- package/src/plugins/builtin/credit-documents/headless.ts +2 -0
- package/src/plugins/builtin/crypto-board/headless.ts +3 -0
- package/src/plugins/builtin/custom-view/loader.ts +1 -1
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -0
- package/src/plugins/builtin/debt-maturities/wall-bar.tsx +14 -41
- package/src/plugins/builtin/dividend-yield/client.ts +4 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +1 -0
- package/src/plugins/builtin/doe/headless.ts +7 -0
- package/src/plugins/builtin/doe/range-bar.tsx +7 -37
- package/src/plugins/builtin/earnings/client.ts +2 -2
- package/src/plugins/builtin/earnings/headless.ts +5 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +1 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +1 -0
- package/src/plugins/builtin/earnings-ripple/client.ts +91 -18
- package/src/plugins/builtin/earnings-ripple/headless.ts +28 -8
- package/src/plugins/builtin/earnings-ripple/index.tsx +2 -2
- package/src/plugins/builtin/earnings-ripple/model.ts +97 -7
- package/src/plugins/builtin/earnings-ripple/pane.tsx +148 -23
- package/src/plugins/builtin/econ/index.tsx +1 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +2 -0
- package/src/plugins/builtin/equity-screener/headless.ts +2 -0
- package/src/plugins/builtin/estimate-revisions/headless.ts +1 -0
- package/src/plugins/builtin/executives/index.tsx +1 -0
- package/src/plugins/builtin/exposure/headless.ts +3 -1
- package/src/plugins/builtin/fear-greed/headless.ts +2 -0
- package/src/plugins/builtin/filing-events/index.tsx +1 -0
- package/src/plugins/builtin/futures-curve/client.ts +12 -3
- package/src/plugins/builtin/futures-curve/headless.ts +39 -10
- package/src/plugins/builtin/futures-curve/index.tsx +3 -3
- package/src/plugins/builtin/futures-curve/model.ts +188 -4
- package/src/plugins/builtin/futures-curve/pane.tsx +51 -9
- package/src/plugins/builtin/fx-matrix/direction.ts +81 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +71 -33
- package/src/plugins/builtin/fx-matrix/live-legs.ts +17 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +68 -14
- package/src/plugins/builtin/fx-matrix/settings.ts +62 -0
- package/src/plugins/builtin/gpu/headless.ts +7 -3
- package/src/plugins/builtin/gpu/ladder.tsx +26 -30
- package/src/plugins/builtin/gpu/model.ts +27 -1
- package/src/plugins/builtin/holders/beneficial-client.ts +209 -0
- package/src/plugins/builtin/holders/beneficial-model.ts +279 -0
- package/src/plugins/builtin/holders/beneficial-report.ts +126 -0
- package/src/plugins/builtin/holders/beneficial-view.tsx +241 -0
- package/src/plugins/builtin/holders/headless.ts +139 -5
- package/src/plugins/builtin/holders/index.tsx +10 -3
- package/src/plugins/builtin/holders/pane.tsx +20 -3
- package/src/plugins/builtin/holders/report-header.ts +95 -0
- package/src/plugins/builtin/holders/table-model.ts +20 -5
- package/src/plugins/builtin/holders/thirteenf-match.ts +29 -1
- package/src/plugins/builtin/holders/types.ts +2 -1
- package/src/plugins/builtin/insider/headless.ts +2 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +1 -0
- package/src/plugins/builtin/iv-history/headless.ts +4 -1
- package/src/plugins/builtin/jobs/headless.ts +1 -0
- package/src/plugins/builtin/jobs/index.tsx +1 -0
- package/src/plugins/builtin/jobs/share-bars.tsx +5 -28
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -0
- package/src/plugins/builtin/macro-day/headless.ts +2 -0
- package/src/plugins/builtin/market-halts/index.tsx +1 -0
- package/src/plugins/builtin/market-heatmap/data.ts +30 -2
- package/src/plugins/builtin/market-heatmap/headless.ts +135 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +245 -81
- package/src/plugins/builtin/market-heatmap/live.ts +88 -0
- package/src/plugins/builtin/market-heatmap/model.ts +146 -0
- package/src/plugins/builtin/market-heatmap/portfolio.ts +50 -11
- package/src/plugins/builtin/market-movers/client.ts +3 -2
- package/src/plugins/builtin/market-movers/headless.ts +10 -1
- package/src/plugins/builtin/market-movers/screener.ts +18 -5
- package/src/plugins/builtin/market-valuation/headless.ts +2 -0
- package/src/plugins/builtin/members/client.ts +53 -0
- package/src/plugins/builtin/members/headless.ts +35 -0
- package/src/plugins/builtin/members/index.tsx +20 -0
- package/src/plugins/builtin/members/model.ts +68 -0
- package/src/plugins/builtin/members/pane.tsx +175 -0
- package/src/plugins/builtin/mna/distress/headless.ts +1 -0
- package/src/plugins/builtin/mna/headless.ts +1 -0
- package/src/plugins/builtin/mna/index.tsx +2 -1
- package/src/plugins/builtin/mna/pane.tsx +5 -2
- package/src/plugins/builtin/money-markets/headless.ts +1 -0
- package/src/plugins/builtin/news/headless.ts +2 -0
- package/src/plugins/builtin/news/wire/index.tsx +8 -1
- package/src/plugins/builtin/notes/cli.ts +14 -6
- package/src/plugins/builtin/notes/index.tsx +1 -0
- package/src/plugins/builtin/openfda/index.tsx +1 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options-calculator/headless.ts +2 -0
- package/src/plugins/builtin/options-positioning/headless.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +5 -0
- package/src/plugins/builtin/pe-band/client.ts +74 -2
- package/src/plugins/builtin/pe-band/headless.ts +26 -8
- package/src/plugins/builtin/pe-band/model.ts +102 -22
- package/src/plugins/builtin/pe-band/pane.tsx +34 -16
- package/src/plugins/builtin/pe-band/report-dates.ts +78 -0
- package/src/plugins/builtin/perps/client.ts +66 -5
- package/src/plugins/builtin/perps/equity-identity.ts +16 -0
- package/src/plugins/builtin/perps/equity-row.tsx +6 -4
- package/src/plugins/builtin/perps/headless.ts +144 -16
- package/src/plugins/builtin/perps/index.tsx +6 -6
- package/src/plugins/builtin/perps/model.ts +189 -12
- package/src/plugins/builtin/perps/pane.tsx +248 -47
- package/src/plugins/builtin/plugin-marketplace/activation.ts +19 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -2
- package/src/plugins/builtin/plugin-marketplace/store.ts +2 -0
- package/src/plugins/builtin/portfolio-list/allocation.ts +245 -0
- package/src/plugins/builtin/portfolio-list/cached-account.ts +33 -0
- package/src/plugins/builtin/portfolio-list/cell-version.ts +19 -1
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +328 -29
- package/src/plugins/builtin/portfolio-list/cli/render.ts +230 -39
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +67 -2
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +35 -31
- package/src/plugins/builtin/portfolio-list/column-values.ts +124 -47
- package/src/plugins/builtin/portfolio-list/headless.ts +98 -16
- package/src/plugins/builtin/portfolio-list/index.tsx +14 -3
- package/src/plugins/builtin/portfolio-list/live-valuation.ts +7 -5
- package/src/plugins/builtin/portfolio-list/mutations.ts +55 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +6 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +47 -13
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +43 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +48 -5
- package/src/plugins/builtin/portfolio-list/settings.ts +33 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +24 -1
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +26 -7
- package/src/plugins/builtin/power/headless.ts +3 -1
- package/src/plugins/builtin/rate-path/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/relative-rotation/headless.ts +2 -0
- package/src/plugins/builtin/research/analyst-headless.ts +3 -0
- package/src/plugins/builtin/research/events-headless.ts +2 -0
- package/src/plugins/builtin/research/headless.ts +2 -0
- package/src/plugins/builtin/research/index.tsx +1 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +10 -5
- package/src/plugins/builtin/research/relative-valuation-model.ts +11 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/research-search/index.tsx +1 -0
- package/src/plugins/builtin/revenue-breakdown/headless.ts +2 -0
- package/src/plugins/builtin/reverse-dcf/headless.ts +2 -0
- package/src/plugins/builtin/risk-factors/headless.ts +2 -0
- package/src/plugins/builtin/scanner/flow-history.ts +2 -13
- package/src/plugins/builtin/scanner/headless.ts +14 -0
- package/src/plugins/builtin/scanner/hilo-bars.tsx +12 -70
- package/src/plugins/builtin/scanner/hilo-model.ts +0 -18
- package/src/plugins/builtin/seasonality/headless.ts +2 -0
- package/src/plugins/builtin/sec/command-bar-search.ts +14 -4
- package/src/plugins/builtin/sec/filing-content.ts +42 -17
- package/src/plugins/builtin/sec/forms.ts +12 -0
- package/src/plugins/builtin/sec/headless.ts +2 -0
- package/src/plugins/builtin/sectors/client.ts +1 -0
- package/src/plugins/builtin/sectors/headless.ts +9 -1
- package/src/plugins/builtin/sectors/move-bar.tsx +10 -32
- package/src/plugins/builtin/sectors/sector-model.ts +2 -0
- package/src/plugins/builtin/shared/crypto-pair.ts +14 -0
- package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
- package/src/plugins/builtin/shared/report-freshness.ts +57 -0
- package/src/plugins/builtin/shared/research-cells.tsx +4 -9
- package/src/plugins/builtin/short-interest/headless.ts +2 -0
- package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
- package/src/plugins/builtin/short-volume/headless.ts +1 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -0
- package/src/plugins/builtin/social-mentions/index.tsx +1 -1
- package/src/plugins/builtin/supply-chain/headless.ts +7 -1
- package/src/plugins/builtin/supply-chain/index.tsx +10 -1
- package/src/plugins/builtin/supply-chain/model.ts +27 -2
- package/src/plugins/builtin/supply-chain/pane.tsx +71 -32
- package/src/plugins/builtin/themes/headless.ts +3 -0
- package/src/plugins/builtin/thirteenf/headless.ts +110 -25
- package/src/plugins/builtin/thirteenf/index.tsx +2 -1
- package/src/plugins/builtin/thirteenf/report-notes.ts +149 -0
- package/src/plugins/builtin/ticker-detail/headless.ts +7 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +5 -60
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +26 -26
- package/src/plugins/builtin/ticker-detail/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +7 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +20 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -2
- package/src/plugins/builtin/time-sales/headless.ts +5 -1
- package/src/plugins/builtin/treasury-auctions/headless.ts +1 -0
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/volatility/headless.ts +6 -1
- package/src/plugins/builtin/world-indices/headless.ts +2 -0
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +157 -0
- package/src/plugins/builtin/world-venue-map/basemap.ts +214 -0
- package/src/plugins/builtin/world-venue-map/client.ts +45 -0
- package/src/plugins/builtin/world-venue-map/entity-detail.tsx +147 -0
- package/src/plugins/builtin/world-venue-map/entity-table.ts +59 -0
- package/src/plugins/builtin/world-venue-map/evidence.ts +77 -0
- package/src/plugins/builtin/world-venue-map/feed.ts +293 -0
- package/src/plugins/builtin/world-venue-map/geo-draw.tsx +248 -0
- package/src/plugins/builtin/world-venue-map/geo-series.ts +142 -0
- package/src/plugins/builtin/world-venue-map/geo-svg.tsx +447 -0
- package/src/plugins/builtin/world-venue-map/headless.ts +146 -0
- package/src/plugins/builtin/world-venue-map/ids.ts +1 -0
- package/src/plugins/builtin/world-venue-map/index.ts +67 -4
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +552 -0
- package/src/plugins/builtin/world-venue-map/layers.ts +348 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +220 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +381 -45
- package/src/plugins/builtin/world-venue-map/model.ts +7 -4
- package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +199 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n75.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n00.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n60.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180s15.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180s30.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180s45.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/world-50m.chunk.ts +4 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +28 -43
- package/src/plugins/builtin/world-venue-map/use-geo.ts +55 -0
- package/src/plugins/builtin/world-venue-map/venue-header.tsx +32 -0
- package/src/plugins/builtin/yield-curve/headless.ts +16 -0
- package/src/plugins/catalog-browser.ts +2 -0
- package/src/plugins/catalog-manifest.ts +1 -0
- package/src/plugins/catalog-ui.ts +3 -1
- package/src/plugins/catalog.ts +1 -1
- package/src/plugins/host-link.ts +33 -1
- package/src/plugins/installer.ts +48 -12
- package/src/plugins/loader.ts +30 -3
- package/src/plugins/manager-node.ts +15 -5
- package/src/plugins/registry/index.ts +1 -1
- package/src/plugins/registry/pane-settings.ts +26 -7
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/build-assets.ts +81 -3
- package/src/renderers/dom/data-table/row.tsx +52 -16
- package/src/renderers/dom/desktop/controls.tsx +7 -3
- package/src/renderers/dom/desktop/icons.tsx +1 -0
- package/src/renderers/dom/desktop/list-view.tsx +35 -10
- package/src/renderers/dom/desktop/popover.tsx +73 -14
- package/src/renderers/dom/host/box.tsx +69 -26
- package/src/renderers/dom/host/input.tsx +14 -3
- package/src/renderers/dom/host/tabs.tsx +37 -1
- package/src/renderers/dom/styles.css +41 -7
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +181 -87
- package/src/renderers/electrobun/bun/index.ts +73 -6
- package/src/renderers/electrobun/bun/window/remembered-frame.ts +122 -0
- package/src/renderers/electrobun/shared/protocol.ts +3 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +10 -4
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -1
- package/src/renderers/electrobun/view/main.tsx +7 -1
- package/src/renderers/electrobun/view/renderer-window-error.ts +18 -0
- package/src/renderers/electrobun/view/rpc-timeout.ts +14 -3
- package/src/renderers/electrobun/view/update-host.ts +45 -21
- package/src/renderers/opentui/host.tsx +5 -0
- package/src/sources/gloomberb-cloud/index.ts +25 -4
- package/src/sources/gloomberb-cloud/normalizers.ts +36 -5
- package/src/sources/provider-router/cached-routes.ts +4 -1
- package/src/sources/provider-router/financials.ts +11 -8
- package/src/sources/provider-router/history.ts +15 -10
- package/src/sources/sec-registrant.ts +75 -0
- package/src/state/app/context/index.tsx +24 -8
- package/src/state/hooks/quote-streaming.ts +41 -26
- package/src/sync/core-contributors.ts +4 -0
- package/src/telemetry/benign-window-error.ts +7 -0
- package/src/telemetry/crash-reports-dom.ts +2 -0
- package/src/telemetry/crash-reports.ts +4 -1
- package/src/test-support/cli-context.ts +10 -2
- package/src/test-support/daily-nav.ts +25 -0
- package/src/theme/heat-colors.ts +357 -2
- package/src/tickers/instrument-kind.ts +34 -7
- package/src/tickers/search/asset-classes.ts +24 -9
- package/src/tickers/search/index.ts +44 -11
- package/src/tickers/search/ranking.ts +23 -3
- package/src/time-series/resolve.ts +1 -1
- package/src/time-series/use-chart-resolution.ts +1 -1
- package/src/types/context-menu.ts +6 -0
- package/src/types/data-provider.ts +2 -0
- package/src/types/financials.ts +5 -0
- package/src/types/headless.ts +79 -2
- package/src/types/plugin.ts +38 -6
- package/src/types/ticker.ts +15 -0
- package/src/ui/context-menu.tsx +1 -0
- package/src/ui/host.tsx +16 -0
- package/src/updater/index.ts +38 -13
- package/src/utils/async-deadline.ts +21 -0
- package/src/utils/cli-output.ts +5 -1
- package/src/utils/crypto-pair.ts +41 -0
- package/src/utils/currency-units.ts +14 -0
- package/src/utils/depositary-receipt.ts +26 -0
- package/src/utils/exchanges.ts +72 -0
- package/src/utils/fire-and-forget.ts +24 -0
- package/src/utils/format.ts +13 -0
- package/src/utils/price-earnings.ts +70 -3
- package/src/utils/price-history.ts +58 -13
- package/src/utils/rpc-timeout-error.ts +27 -0
- package/src/utils/sec.ts +13 -1
- package/src/utils/utc-time.ts +79 -0
|
@@ -1,145 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type ScrollBoxRenderable,
|
|
14
|
-
} from "../../../ui";
|
|
15
|
-
import { useShortcut } from "../../../react/input";
|
|
16
|
-
import { usePaneFooter } from "../../layout/pane/footer/registration";
|
|
17
|
-
import type { PaneHint } from "../../layout/pane/footer/model";
|
|
18
|
-
import type { ContextMenuItem } from "../../../types/context-menu";
|
|
19
|
-
import { useOptionalPaneInstanceId, usePaneSettingValue } from "../../../state/app/context";
|
|
20
|
-
import { colors as themeColors, hoverBg } from "../../../theme/colors";
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCompositeChartKeyboard } from "./use-composite-chart-keyboard";
|
|
3
|
+
import { useCompositeViewport } from "./use-composite-viewport";
|
|
4
|
+
import { CompositePanelSurface } from "./panel-surface";
|
|
5
|
+
import { MINIMUM_AXIS_LABEL_WIDTH, compositeAxisLabelWidth } from "./panel-labels";
|
|
6
|
+
import { isVerticalWheelDirection } from "./panel-gestures";
|
|
7
|
+
import { type ChartToolSpan, CHART_DRAWING_COLORS, isDrawingTool, type ChartDrawing } from "./tools";
|
|
8
|
+
import { ChartDrawingStore, NO_DRAWINGS } from "./drawing-store";
|
|
9
|
+
import { CompositeLegend } from "./legend";
|
|
10
|
+
import { ChartToolbar, CHART_TOOLBAR_WIDTH } from "./toolbar";
|
|
11
|
+
import { AsciiText, Box, ChartSurface, Text, useUiCapabilities } from "../../../ui";
|
|
12
|
+
import { useOptionalPaneInstanceId } from "../../../state/app/context";
|
|
21
13
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
22
14
|
import { chartSurfaceBackground, usePaneSurface } from "../../layout/pane/surface";
|
|
23
|
-
import { CANONICAL_EXCHANGE_ALIASES } from "../../../utils/exchanges";
|
|
24
|
-
import { displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
|
|
25
|
-
import { isPlainKey } from "../../../utils/keyboard";
|
|
26
15
|
import { CHART_WATERMARK_ROLE } from "../../../utils/screenshot-watermark";
|
|
27
16
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
28
17
|
import { downsampleCompositeChartScene } from "./downsample";
|
|
29
18
|
import { reuseResolvedSeriesList } from "./panel-series";
|
|
30
|
-
import {
|
|
31
|
-
consumeChartMouseEvent,
|
|
32
|
-
getGlobalMouseX,
|
|
33
|
-
getLocalPlotPointer,
|
|
34
|
-
type ChartMouseEvent,
|
|
35
|
-
} from "../core/pointer";
|
|
36
|
-
import type { NativeChartBitmap } from "../native/chart-rasterizer";
|
|
19
|
+
import { consumeChartMouseEvent, type ChartMouseEvent } from "../core/pointer";
|
|
37
20
|
import { useShowChartTextFallback } from "../native/use-chart-text-fallback";
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} from "./
|
|
42
|
-
import {
|
|
43
|
-
StaticXAxisLabels,
|
|
44
|
-
StaticXMarkerLabels,
|
|
45
|
-
StaticXMarkerOverlay,
|
|
46
|
-
} from "./axis-overlays";
|
|
47
|
-
import { PriceAxisLabels } from "./price-axis-labels";
|
|
48
|
-
import {
|
|
49
|
-
compositeAxisTicks,
|
|
50
|
-
formatCompositeCursorValue,
|
|
51
|
-
formatCompositePointDetails,
|
|
52
|
-
formatCompositeSeriesValue,
|
|
53
|
-
seriesPriceReference,
|
|
54
|
-
formatCompositeTimeAxisDate,
|
|
55
|
-
type CompositeAxisValueFormatter,
|
|
56
|
-
} from "./format";
|
|
57
|
-
import {
|
|
58
|
-
COMPOSITE_KEYBOARD_PAN_RATIO,
|
|
59
|
-
COMPOSITE_ZOOM_STEP_FACTOR,
|
|
60
|
-
buildCompositeNavigationFrame,
|
|
61
|
-
compositeNavigationDataViewport,
|
|
62
|
-
compositeViewportPositions,
|
|
63
|
-
fitCompositeViewport,
|
|
64
|
-
panCompositeViewport,
|
|
65
|
-
resolveCompositeChartInteraction,
|
|
66
|
-
resolveCompositeWheelPan,
|
|
67
|
-
resolveCompositeWheelZoom,
|
|
68
|
-
sameCompositeViewport,
|
|
69
|
-
shouldResetCompositeViewport,
|
|
70
|
-
zoomCompositeViewport,
|
|
71
|
-
type CompositeNavigationFrame,
|
|
72
|
-
type CompositeViewportRange,
|
|
73
|
-
} from "./interactions";
|
|
74
|
-
import { buildCompositeColumnLayout, type CompositeColumnLayout } from "./column-layout";
|
|
75
|
-
import { renderCompositePanelBitmap } from "./rasterizer";
|
|
76
|
-
import {
|
|
77
|
-
fitAxisLabels,
|
|
78
|
-
hitTestLevel,
|
|
79
|
-
levelGrabRatio,
|
|
80
|
-
levelPanel,
|
|
81
|
-
levelVectors,
|
|
82
|
-
paintLevels,
|
|
83
|
-
projectLevels,
|
|
84
|
-
roundLevelValue,
|
|
85
|
-
writeLevelText,
|
|
86
|
-
type CompositeChartLevel,
|
|
87
|
-
type CompositeChartLevels,
|
|
88
|
-
type ProjectedLevel,
|
|
89
|
-
} from "./levels";
|
|
90
|
-
import {
|
|
91
|
-
buildChartToolVectors,
|
|
92
|
-
CHART_DRAWING_COLORS,
|
|
93
|
-
CHART_DRAWINGS_SETTING_KEY,
|
|
94
|
-
countMeasureBars,
|
|
95
|
-
drawChartToolOverlay,
|
|
96
|
-
resolveChartToolKind,
|
|
97
|
-
resolveMeasureAxisDomain,
|
|
98
|
-
resolveMeasureDirection,
|
|
99
|
-
resolveMeasureValueAt,
|
|
100
|
-
resolveZoomTimeRange,
|
|
101
|
-
hitTestDrawings,
|
|
102
|
-
isDrawingTool,
|
|
103
|
-
nextDrawingColor,
|
|
104
|
-
parseChartDrawings,
|
|
105
|
-
resolveDrawingFromDrag,
|
|
106
|
-
resolveZoomBoxRange,
|
|
107
|
-
shiftDrawing,
|
|
108
|
-
summarizeMeasure,
|
|
109
|
-
summarizeZoomRange,
|
|
110
|
-
summarizeZoomSelection,
|
|
111
|
-
type ChartDrawing,
|
|
112
|
-
type ChartDrawingPoint,
|
|
113
|
-
type ChartToolDrag,
|
|
114
|
-
type ChartToolKind,
|
|
115
|
-
} from "./tools";
|
|
116
|
-
import {
|
|
117
|
-
COMPOSITE_RIGHT_OFFSET_RATIO,
|
|
118
|
-
compositeRightOffsetRatio,
|
|
119
|
-
projectCompositeTimestamp,
|
|
120
|
-
unprojectCompositeTimestamp,
|
|
121
|
-
} from "./time-scale";
|
|
21
|
+
import { StaticXAxisLabels, StaticXMarkerLabels, StaticXMarkerOverlay } from "./axis-overlays";
|
|
22
|
+
import { formatCompositeTimeAxisDate } from "./format";
|
|
23
|
+
import { resolveCompositeWheelPan, resolveCompositeWheelZoom } from "./interactions";
|
|
24
|
+
import { COMPOSITE_RIGHT_OFFSET_RATIO } from "./time-scale";
|
|
122
25
|
import {
|
|
123
26
|
allocateCompositePanelHeights,
|
|
124
27
|
applyCompositeChartCursor,
|
|
125
28
|
buildCompositeChartScene,
|
|
126
|
-
projectCompositeValue,
|
|
127
29
|
resizeCompositePanel,
|
|
128
|
-
resolveAdjacentCompositeCursorDate,
|
|
129
|
-
resolveCompositeCursorDate,
|
|
130
|
-
unprojectCompositeValue,
|
|
131
30
|
} from "./scene";
|
|
132
|
-
import {
|
|
133
|
-
compositeAxisTickLabels,
|
|
134
|
-
renderCompositeAxisText,
|
|
135
|
-
renderCompositePanelText,
|
|
136
|
-
} from "./text-renderer";
|
|
137
|
-
import {
|
|
138
|
-
buildCompositeTimeAxisLayout,
|
|
139
|
-
buildCompositeViewportTimeAxisLayout,
|
|
140
|
-
} from "./time-axis";
|
|
31
|
+
import { buildCompositeTimeAxisLayout, buildCompositeViewportTimeAxisLayout } from "./time-axis";
|
|
141
32
|
import type {
|
|
142
|
-
CompositeAxisDomain,
|
|
143
33
|
CompositeChartColors,
|
|
144
34
|
CompositeChartProps,
|
|
145
35
|
CompositeChartScene,
|
|
@@ -147,408 +37,6 @@ import type {
|
|
|
147
37
|
CompositePanelScene,
|
|
148
38
|
} from "./types";
|
|
149
39
|
|
|
150
|
-
// A short resize-only delay coalesces geometry churn without delaying
|
|
151
|
-
// live-data paints or depending on a foreground animation frame.
|
|
152
|
-
const DESKTOP_BITMAP_RESIZE_DEBOUNCE_MS = 32;
|
|
153
|
-
const LEGEND_WHEEL_DELTA_PER_CELL = 8;
|
|
154
|
-
/** How far past the first loaded observation a backfilling chart may pan, as a fraction of the view. */
|
|
155
|
-
const HISTORICAL_PADDING_RATIO = 0.5;
|
|
156
|
-
|
|
157
|
-
function isVerticalWheelDirection(
|
|
158
|
-
direction: "up" | "down" | "left" | "right",
|
|
159
|
-
): direction is "up" | "down" {
|
|
160
|
-
return direction === "up" || direction === "down";
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
interface PanGesture {
|
|
164
|
-
kind: "pan";
|
|
165
|
-
startGlobalX: number;
|
|
166
|
-
frame: CompositeNavigationFrame;
|
|
167
|
-
startViewport: CompositeViewportRange;
|
|
168
|
-
positionsPerCell: number;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* A drag pans in the frame it started in; see `CompositeNavigationFrame`. The
|
|
173
|
-
* plot covers the viewport plus its reserved right offset, so a cell is worth
|
|
174
|
-
* that much time and the bars keep pace with the pointer.
|
|
175
|
-
*/
|
|
176
|
-
function startPanGesture(
|
|
177
|
-
frame: CompositeNavigationFrame,
|
|
178
|
-
viewport: CompositeViewportRange,
|
|
179
|
-
plotWidth: number,
|
|
180
|
-
globalX: number,
|
|
181
|
-
plotSpanFactor: number,
|
|
182
|
-
): PanGesture {
|
|
183
|
-
const positions = compositeViewportPositions(frame, viewport);
|
|
184
|
-
const span = positions ? Math.max(positions.end - positions.start, Number.EPSILON) : 1;
|
|
185
|
-
return {
|
|
186
|
-
kind: "pan",
|
|
187
|
-
startGlobalX: globalX,
|
|
188
|
-
frame,
|
|
189
|
-
startViewport: viewport,
|
|
190
|
-
positionsPerCell: span * plotSpanFactor / Math.max(plotWidth, 1),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
interface PendingWheel {
|
|
195
|
-
/** Fraction of the visible span, positive toward older observations. */
|
|
196
|
-
panRatio: number;
|
|
197
|
-
zoomLog: number;
|
|
198
|
-
anchorRatio: number;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const webFrame = globalThis as typeof globalThis & {
|
|
202
|
-
requestAnimationFrame?: (callback: () => void) => number;
|
|
203
|
-
cancelAnimationFrame?: (handle: number) => void;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
function renderPanelBitmap(
|
|
207
|
-
panel: CompositePanelScene,
|
|
208
|
-
bitmapSize: StaticChartBitmapSize,
|
|
209
|
-
colors: CompositeChartColors,
|
|
210
|
-
snapGridToRows = false,
|
|
211
|
-
): NativeChartBitmap {
|
|
212
|
-
return renderCompositePanelBitmap(panel, {
|
|
213
|
-
pixelWidth: bitmapSize.pixelWidth,
|
|
214
|
-
pixelHeight: bitmapSize.pixelHeight,
|
|
215
|
-
colors,
|
|
216
|
-
snapGridToRows,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function useCompositePanelBitmap({
|
|
221
|
-
panel,
|
|
222
|
-
bitmapSize,
|
|
223
|
-
colors,
|
|
224
|
-
isDesktopWeb,
|
|
225
|
-
}: {
|
|
226
|
-
panel: CompositePanelScene;
|
|
227
|
-
bitmapSize: StaticChartBitmapSize | null;
|
|
228
|
-
colors: CompositeChartColors;
|
|
229
|
-
isDesktopWeb: boolean;
|
|
230
|
-
}): NativeChartBitmap | null {
|
|
231
|
-
const [desktopBitmap, setDesktopBitmap] = useState<NativeChartBitmap | null>(null);
|
|
232
|
-
const desktopBitmapRef = useRef<NativeChartBitmap | null>(null);
|
|
233
|
-
const desktopRenderInputRef = useRef<{
|
|
234
|
-
panel: CompositePanelScene;
|
|
235
|
-
pixelWidth: number;
|
|
236
|
-
pixelHeight: number;
|
|
237
|
-
colors: CompositeChartColors;
|
|
238
|
-
} | null>(null);
|
|
239
|
-
const desktopRequestedSizeRef = useRef<{ pixelWidth: number; pixelHeight: number } | null>(null);
|
|
240
|
-
const desktopRenderedSizeRef = useRef<{ pixelWidth: number; pixelHeight: number } | null>(null);
|
|
241
|
-
const desktopRenderTimerRef = useRef<ReturnType<typeof globalThis.setTimeout> | null>(null);
|
|
242
|
-
const desktopActiveRef = useRef(false);
|
|
243
|
-
const pixelWidth = bitmapSize?.pixelWidth ?? null;
|
|
244
|
-
const pixelHeight = bitmapSize?.pixelHeight ?? null;
|
|
245
|
-
|
|
246
|
-
desktopRenderInputRef.current = isDesktopWeb && pixelWidth !== null && pixelHeight !== null
|
|
247
|
-
? { panel, pixelWidth, pixelHeight, colors }
|
|
248
|
-
: null;
|
|
249
|
-
|
|
250
|
-
// The cursor is drawn as a separate overlay, so the plot raster stays cached
|
|
251
|
-
// (and resident in the terminal) while the crosshair moves.
|
|
252
|
-
const terminalBitmap = useMemo(() => {
|
|
253
|
-
if (isDesktopWeb || !bitmapSize) return null;
|
|
254
|
-
return renderPanelBitmap(panel, bitmapSize, colors, true);
|
|
255
|
-
}, [bitmapSize, colors, isDesktopWeb, panel]);
|
|
256
|
-
|
|
257
|
-
useEffect(() => {
|
|
258
|
-
const cancelRender = () => {
|
|
259
|
-
if (desktopRenderTimerRef.current === null) return;
|
|
260
|
-
clearTimeout(desktopRenderTimerRef.current);
|
|
261
|
-
desktopRenderTimerRef.current = null;
|
|
262
|
-
};
|
|
263
|
-
const scheduleRender = (delay: number) => {
|
|
264
|
-
if (desktopRenderTimerRef.current !== null) return;
|
|
265
|
-
desktopRenderTimerRef.current = globalThis.setTimeout(() => {
|
|
266
|
-
desktopRenderTimerRef.current = null;
|
|
267
|
-
if (!desktopActiveRef.current) return;
|
|
268
|
-
const input = desktopRenderInputRef.current;
|
|
269
|
-
if (!input) return;
|
|
270
|
-
const next = renderPanelBitmap(
|
|
271
|
-
input.panel,
|
|
272
|
-
{ pixelWidth: input.pixelWidth, pixelHeight: input.pixelHeight },
|
|
273
|
-
input.colors,
|
|
274
|
-
);
|
|
275
|
-
if (!desktopActiveRef.current) return;
|
|
276
|
-
desktopRenderedSizeRef.current = {
|
|
277
|
-
pixelWidth: input.pixelWidth,
|
|
278
|
-
pixelHeight: input.pixelHeight,
|
|
279
|
-
};
|
|
280
|
-
desktopBitmapRef.current = next;
|
|
281
|
-
setDesktopBitmap(next);
|
|
282
|
-
}, delay);
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
if (!isDesktopWeb) {
|
|
286
|
-
desktopActiveRef.current = false;
|
|
287
|
-
cancelRender();
|
|
288
|
-
desktopRequestedSizeRef.current = null;
|
|
289
|
-
desktopRenderedSizeRef.current = null;
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (pixelWidth === null || pixelHeight === null) {
|
|
293
|
-
desktopActiveRef.current = false;
|
|
294
|
-
cancelRender();
|
|
295
|
-
desktopRequestedSizeRef.current = null;
|
|
296
|
-
desktopRenderedSizeRef.current = null;
|
|
297
|
-
desktopBitmapRef.current = null;
|
|
298
|
-
setDesktopBitmap((current) => current === null ? current : null);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
desktopActiveRef.current = true;
|
|
303
|
-
const nextSize = { pixelWidth, pixelHeight };
|
|
304
|
-
const requestedSize = desktopRequestedSizeRef.current;
|
|
305
|
-
const requestedSizeChanged = !requestedSize
|
|
306
|
-
|| requestedSize.pixelWidth !== pixelWidth
|
|
307
|
-
|| requestedSize.pixelHeight !== pixelHeight;
|
|
308
|
-
desktopRequestedSizeRef.current = nextSize;
|
|
309
|
-
const renderedSize = desktopRenderedSizeRef.current;
|
|
310
|
-
const sizeAlreadyRendered = !!renderedSize
|
|
311
|
-
&& renderedSize.pixelWidth === pixelWidth
|
|
312
|
-
&& renderedSize.pixelHeight === pixelHeight;
|
|
313
|
-
|
|
314
|
-
if (!desktopBitmapRef.current || sizeAlreadyRendered) {
|
|
315
|
-
if (requestedSizeChanged) cancelRender();
|
|
316
|
-
scheduleRender(0);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (!requestedSizeChanged || desktopRenderTimerRef.current !== null) return;
|
|
321
|
-
scheduleRender(DESKTOP_BITMAP_RESIZE_DEBOUNCE_MS);
|
|
322
|
-
}, [colors, isDesktopWeb, panel, pixelHeight, pixelWidth]);
|
|
323
|
-
|
|
324
|
-
useEffect(() => () => {
|
|
325
|
-
desktopActiveRef.current = false;
|
|
326
|
-
if (desktopRenderTimerRef.current !== null) {
|
|
327
|
-
clearTimeout(desktopRenderTimerRef.current);
|
|
328
|
-
desktopRenderTimerRef.current = null;
|
|
329
|
-
}
|
|
330
|
-
}, []);
|
|
331
|
-
|
|
332
|
-
if (!bitmapSize) return null;
|
|
333
|
-
return isDesktopWeb ? desktopBitmap : terminalBitmap;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function resolvePanelCrosshair(
|
|
337
|
-
panel: CompositePanelScene,
|
|
338
|
-
columnLayout: CompositeColumnLayout,
|
|
339
|
-
bitmap: NativeChartBitmap | null,
|
|
340
|
-
cursorXRatio: number | null,
|
|
341
|
-
cursorYRatio: number | null,
|
|
342
|
-
color: string,
|
|
343
|
-
): ChartSurfaceProps["crosshair"] {
|
|
344
|
-
if (!bitmap || cursorXRatio === null) return null;
|
|
345
|
-
const markers = panel.series.flatMap((series) => {
|
|
346
|
-
// Column cohorts are drawn at their group center, not each observation's
|
|
347
|
-
// own timestamp, so match the position the bar actually occupies.
|
|
348
|
-
const cursorPoint = series.points.find((point) => {
|
|
349
|
-
const xRatio = series.source.style === "columns"
|
|
350
|
-
? columnLayout.groupByPoint.get(point)?.xRatio ?? point.xRatio
|
|
351
|
-
: point.xRatio;
|
|
352
|
-
return Math.abs(xRatio - cursorXRatio) < 1e-9;
|
|
353
|
-
});
|
|
354
|
-
return cursorPoint
|
|
355
|
-
? [{
|
|
356
|
-
pixelY: cursorPoint.yRatio * Math.max(bitmap.height - 1, 0),
|
|
357
|
-
color: series.source.color,
|
|
358
|
-
}]
|
|
359
|
-
: [];
|
|
360
|
-
});
|
|
361
|
-
return {
|
|
362
|
-
pixelX: cursorXRatio * Math.max(bitmap.width - 1, 0),
|
|
363
|
-
pixelY: cursorYRatio === null ? null : cursorYRatio * Math.max(bitmap.height - 1, 0),
|
|
364
|
-
color,
|
|
365
|
-
markers,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function axisLabelRows(lines: string[]): ReadonlyMap<number, string> {
|
|
370
|
-
return new Map(lines.flatMap((line, row) => {
|
|
371
|
-
const label = line.trim();
|
|
372
|
-
return label ? [[row, label] as const] : [];
|
|
373
|
-
}));
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function cursorAxisLabel(
|
|
377
|
-
panel: CompositePanelScene,
|
|
378
|
-
side: "left" | "right",
|
|
379
|
-
cursorYRatio: number | null,
|
|
380
|
-
format?: CompositeAxisValueFormatter,
|
|
381
|
-
): string | null {
|
|
382
|
-
const domain = panel.axes[side];
|
|
383
|
-
if (!domain || cursorYRatio === null) return null;
|
|
384
|
-
const value = unprojectCompositeValue(cursorYRatio, domain);
|
|
385
|
-
if (value === null) return null;
|
|
386
|
-
return format ? format(value, domain) : formatCompositeCursorValue(value, domain);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const MINIMUM_AXIS_LABEL_WIDTH = 3;
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Reserve cursor precision even when the tick values are round numbers. This
|
|
393
|
-
* keeps the plot stable while the pointer moves between integer tick values.
|
|
394
|
-
*/
|
|
395
|
-
function compositeAxisLabelWidth(
|
|
396
|
-
domain: CompositeAxisDomain | undefined,
|
|
397
|
-
format: CompositeAxisValueFormatter | undefined,
|
|
398
|
-
includeCursor: boolean,
|
|
399
|
-
): number {
|
|
400
|
-
if (!domain) return 0;
|
|
401
|
-
const ticks = compositeAxisTicks(domain, format);
|
|
402
|
-
const labels = ticks.map((tick) => tick.label);
|
|
403
|
-
if (includeCursor) {
|
|
404
|
-
const cursorFormat = format ?? formatCompositeCursorValue;
|
|
405
|
-
for (const { value } of ticks) {
|
|
406
|
-
labels.push(cursorFormat(value, domain));
|
|
407
|
-
// Totals use two decimals at their compact scale; prices can use four
|
|
408
|
-
// below one currency unit. Probe those digits without changing the data.
|
|
409
|
-
const scale = domain.unitGroup.toLowerCase().split(":")[0] === "currency-total"
|
|
410
|
-
? 10 ** Math.max(0, Math.min(12, Math.floor(Math.log10(Math.abs(value) || 1) / 3) * 3))
|
|
411
|
-
: Math.abs(value) > 0 && Math.abs(value) < 0.01
|
|
412
|
-
? 10 ** Math.floor(Math.log10(Math.abs(value))) : 1;
|
|
413
|
-
const precisionValue = (value < 0 ? -1 : 1) * (Math.floor(Math.abs(value) / scale) + 0.1234) * scale;
|
|
414
|
-
labels.push(cursorFormat(precisionValue, domain));
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return labels.reduce((widest, label) => Math.max(widest, [...label].length), 0);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const HAND_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M6.4 8V3.5a1.15 1.15 0 0 1 2.3 0V7m0-.6a1.15 1.15 0 0 1 2.3 0V8m0-.5a1.15 1.15 0 0 1 2.3 0v3.1c0 2.1-1.7 3.8-3.8 3.8H8.9c-1.2 0-2.3-.6-3-1.6L3.6 9.4a1.15 1.15 0 0 1 1.8-1.4L6.4 9.2" fill="none" stroke="#000" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
422
|
-
const RULER_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="1.4" y="4.6" width="13.2" height="6.8" rx="1.4" fill="none" stroke="#000" stroke-width="1.4"/><path d="M5 4.6v2.6M8 4.6v3.6M11 4.6v2.6" stroke="#000" stroke-width="1.3" stroke-linecap="round"/></svg>`;
|
|
423
|
-
const PEN_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M2.4 13.6 4 9.9 10.6 3.3a1.6 1.6 0 0 1 2.3 0l0 0a1.6 1.6 0 0 1 0 2.3L6.2 12 2.4 13.6Z" fill="none" stroke="#000" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
|
|
424
|
-
const LINE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3.2 12.8 12.8 3.2" stroke="#000" stroke-width="1.6" stroke-linecap="round"/><circle cx="3.2" cy="12.8" r="2" fill="none" stroke="#000" stroke-width="1.4"/><circle cx="12.8" cy="3.2" r="2" fill="none" stroke="#000" stroke-width="1.4"/></svg>`;
|
|
425
|
-
const LEVEL_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M1.6 8h4.2M10.2 8h4.2" stroke="#000" stroke-width="1.6" stroke-linecap="round"/><circle cx="8" cy="8" r="2.2" fill="none" stroke="#000" stroke-width="1.4"/></svg>`;
|
|
426
|
-
const MARQUEE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M2.2 6V3.4a1.2 1.2 0 0 1 1.2-1.2H6M10 2.2h2.6a1.2 1.2 0 0 1 1.2 1.2V6M13.8 10v2.6a1.2 1.2 0 0 1-1.2 1.2H10M6 13.8H3.4a1.2 1.2 0 0 1-1.2-1.2V10" fill="none" stroke="#000" stroke-width="1.7" stroke-linecap="round"/></svg>`;
|
|
427
|
-
|
|
428
|
-
const CHART_TOOLS: ReadonlyArray<{
|
|
429
|
-
/** null is the resting state: the pointer pans and nothing is armed. */
|
|
430
|
-
kind: ChartToolKind | null;
|
|
431
|
-
label: string;
|
|
432
|
-
shortcut: string;
|
|
433
|
-
hint: string;
|
|
434
|
-
glyph: string;
|
|
435
|
-
icon: string;
|
|
436
|
-
}> = [
|
|
437
|
-
{
|
|
438
|
-
kind: null,
|
|
439
|
-
label: "Pan",
|
|
440
|
-
shortcut: "Esc",
|
|
441
|
-
hint: "Drag to move through time, the resting state of the pointer",
|
|
442
|
-
glyph: "\u2725",
|
|
443
|
-
icon: HAND_ICON,
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
kind: "measure",
|
|
447
|
-
label: "Ruler",
|
|
448
|
-
shortcut: "Shift+M",
|
|
449
|
-
hint: "Drag to measure change, percent, bars, and elapsed time",
|
|
450
|
-
glyph: "\u2194",
|
|
451
|
-
icon: RULER_ICON,
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
kind: "zoom",
|
|
455
|
-
label: "Zoom to range",
|
|
456
|
-
shortcut: "Shift+Z",
|
|
457
|
-
hint: "Drag to select a time range to zoom into",
|
|
458
|
-
glyph: "\u229e",
|
|
459
|
-
icon: MARQUEE_ICON,
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
kind: "line",
|
|
463
|
-
label: "Trend line",
|
|
464
|
-
shortcut: "Shift+D",
|
|
465
|
-
hint: "Drag a straight line, grab an end to reshape it, Backspace deletes",
|
|
466
|
-
glyph: "\u2571",
|
|
467
|
-
icon: LINE_ICON,
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
kind: "pencil",
|
|
471
|
-
label: "Freehand",
|
|
472
|
-
shortcut: "Shift+P",
|
|
473
|
-
hint: "Draw freehand, drag a shape to move it, Backspace deletes",
|
|
474
|
-
glyph: "\u223f",
|
|
475
|
-
icon: PEN_ICON,
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
kind: "level",
|
|
479
|
-
label: "Price level",
|
|
480
|
-
shortcut: "Shift+H",
|
|
481
|
-
hint: "Click to add a level every chart of this ticker shows, drag one to move it, Backspace deletes",
|
|
482
|
-
glyph: "\u2550",
|
|
483
|
-
icon: LEVEL_ICON,
|
|
484
|
-
},
|
|
485
|
-
];
|
|
486
|
-
|
|
487
|
-
interface ChartToolSpan {
|
|
488
|
-
startXRatio: number;
|
|
489
|
-
endXRatio: number;
|
|
490
|
-
/** The anchor's own time, which can sit outside the view once it pans. */
|
|
491
|
-
startTime: number;
|
|
492
|
-
color: string;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/** Pane menu names for the tools, with the keys that pick them. */
|
|
496
|
-
const CHART_TOOL_MENU: ReadonlyArray<{ kind: ChartToolKind; label: string; accelerator: string }> = [
|
|
497
|
-
{ kind: "measure", label: "Ruler", accelerator: "Shift+M" },
|
|
498
|
-
{ kind: "zoom", label: "Zoom to Range", accelerator: "Shift+Z" },
|
|
499
|
-
{ kind: "line", label: "Trend Line", accelerator: "Shift+D" },
|
|
500
|
-
{ kind: "pencil", label: "Freehand", accelerator: "Shift+P" },
|
|
501
|
-
{ kind: "level", label: "Price Level", accelerator: "Shift+H" },
|
|
502
|
-
];
|
|
503
|
-
|
|
504
|
-
/** What Enter does next with a tool in hand, for the footer and the pane menu. */
|
|
505
|
-
const KEYBOARD_TOOL_HINTS: Record<ChartToolKind, {
|
|
506
|
-
start: string;
|
|
507
|
-
startTitle: string;
|
|
508
|
-
finish: string;
|
|
509
|
-
finishTitle: string;
|
|
510
|
-
}> = {
|
|
511
|
-
measure: { start: "measure", startTitle: "Start Measure", finish: "done", finishTitle: "Finish Measure" },
|
|
512
|
-
zoom: { start: "select", startTitle: "Select Range", finish: "zoom", finishTitle: "Zoom to Range" },
|
|
513
|
-
line: { start: "draw", startTitle: "Draw Line", finish: "place", finishTitle: "Place Line" },
|
|
514
|
-
pencil: { start: "draw", startTitle: "Draw Freehand", finish: "place", finishTitle: "Place Drawing" },
|
|
515
|
-
level: { start: "add level", startTitle: "Add Level", finish: "add level", finishTitle: "Add Level" },
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* A tool placed from the keyboard: Enter anchors it at the cursor, the arrows
|
|
520
|
-
* move its end and Enter finishes it. Held in data, so a pan, a zoom or a new
|
|
521
|
-
* bar leaves it on the observations it was placed on.
|
|
522
|
-
*/
|
|
523
|
-
interface KeyboardToolPlacement {
|
|
524
|
-
kind: ChartToolKind;
|
|
525
|
-
panelId: string;
|
|
526
|
-
start: ChartDrawingPoint;
|
|
527
|
-
end: ChartDrawingPoint;
|
|
528
|
-
/** Every step the end took: the freehand tool's trail. */
|
|
529
|
-
path: ChartDrawingPoint[];
|
|
530
|
-
/** Up or Down set the end's level, so Left and Right stop following the series. */
|
|
531
|
-
freeValue: boolean;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/** A keyboard placement projected into the plot of the panel that holds it. */
|
|
535
|
-
interface KeyboardToolDrag {
|
|
536
|
-
panelId: string;
|
|
537
|
-
drag: ChartToolDrag;
|
|
538
|
-
start: ChartDrawingPoint;
|
|
539
|
-
end: ChartDrawingPoint;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const ARMED_TOOL_BY_INTERACTION = {
|
|
543
|
-
"arm-measure": "measure",
|
|
544
|
-
"arm-zoom": "zoom",
|
|
545
|
-
"arm-line": "line",
|
|
546
|
-
"arm-pencil": "pencil",
|
|
547
|
-
"arm-level": "level",
|
|
548
|
-
} as const satisfies Record<string, ChartToolKind>;
|
|
549
|
-
|
|
550
|
-
const COMPOSITE_PANEL_ROLE = "composite-chart-panel";
|
|
551
|
-
|
|
552
40
|
// Wordmark cell grid at scale 1 (27 columns of 8px, 4 rows of 12px).
|
|
553
41
|
const WATERMARK_BASE_WIDTH_PX = 216;
|
|
554
42
|
const WATERMARK_BASE_HEIGHT_PX = 48;
|
|
@@ -567,1385 +55,7 @@ function chartWatermarkScale(plotWidthPx: number, plotHeightPx: number): number
|
|
|
567
55
|
return scale >= 1 ? Math.round(scale * 4) / 4 : null;
|
|
568
56
|
}
|
|
569
57
|
|
|
570
|
-
let nextDrawingSequence = 1;
|
|
571
|
-
|
|
572
|
-
function nextDrawingId(): string {
|
|
573
|
-
return `drawing:${nextDrawingSequence++}`;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/** Levels are stored with the account, so their ids must not repeat across sessions. */
|
|
577
|
-
function nextLevelId(): string {
|
|
578
|
-
return `${Date.now().toString(36)}-${(nextDrawingSequence++).toString(36)}`;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const NO_LEVELS: readonly ProjectedLevel[] = [];
|
|
582
|
-
|
|
583
|
-
/** Shift and a letter, whether or not the terminal reports the shift itself. */
|
|
584
|
-
function isShiftedLetter(event: { name?: string; shift?: boolean; ctrl?: boolean; meta?: boolean; alt?: boolean }, letter: string): boolean {
|
|
585
|
-
if (event.ctrl || event.meta || event.alt) return false;
|
|
586
|
-
return event.name === letter.toUpperCase() || (event.shift === true && event.name === letter);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/** Icon cells plus the gap between chips. */
|
|
590
|
-
const CHART_TOOLBAR_WIDTH = CHART_TOOLS.length * 3 + (CHART_TOOLS.length - 1);
|
|
591
|
-
|
|
592
|
-
function ChartToolChip({
|
|
593
|
-
tool,
|
|
594
|
-
active,
|
|
595
|
-
isDesktopWeb,
|
|
596
|
-
onPress,
|
|
597
|
-
}: {
|
|
598
|
-
tool: (typeof CHART_TOOLS)[number];
|
|
599
|
-
active: boolean;
|
|
600
|
-
isDesktopWeb: boolean;
|
|
601
|
-
onPress: () => void;
|
|
602
|
-
}) {
|
|
603
|
-
const label = `${tool.label} (${tool.shortcut}). ${tool.hint}`;
|
|
604
|
-
const color = active ? themeColors.text : themeColors.textDim;
|
|
605
|
-
return (
|
|
606
|
-
<Box
|
|
607
|
-
flexDirection="row"
|
|
608
|
-
alignItems="center"
|
|
609
|
-
justifyContent="center"
|
|
610
|
-
width={3}
|
|
611
|
-
height={1}
|
|
612
|
-
flexShrink={0}
|
|
613
|
-
backgroundColor={active ? themeColors.selected : undefined}
|
|
614
|
-
hoverBackgroundColor={hoverBg()}
|
|
615
|
-
onMouseDown={(event: ChartMouseEvent) => {
|
|
616
|
-
consumeChartMouseEvent(event);
|
|
617
|
-
onPress();
|
|
618
|
-
}}
|
|
619
|
-
cursor="pointer"
|
|
620
|
-
data-gloom-interactive="true"
|
|
621
|
-
data-gloom-role="composite-chart-tool"
|
|
622
|
-
data-gloom-label={label}
|
|
623
|
-
data-active={active ? "true" : "false"}
|
|
624
|
-
title={isDesktopWeb ? label : undefined}
|
|
625
|
-
// Cells size the terminal strip; the desktop chip sizes to its icon.
|
|
626
|
-
style={isDesktopWeb ? { width: "auto", paddingInline: 4, borderRadius: 4 } : undefined}
|
|
627
|
-
>
|
|
628
|
-
{isDesktopWeb ? (
|
|
629
|
-
<Box
|
|
630
|
-
flexShrink={0}
|
|
631
|
-
style={{
|
|
632
|
-
width: 13,
|
|
633
|
-
height: 13,
|
|
634
|
-
backgroundColor: color,
|
|
635
|
-
maskImage: svgMaskUrl(tool.icon),
|
|
636
|
-
WebkitMaskImage: svgMaskUrl(tool.icon),
|
|
637
|
-
maskSize: "contain",
|
|
638
|
-
WebkitMaskSize: "contain",
|
|
639
|
-
maskRepeat: "no-repeat",
|
|
640
|
-
WebkitMaskRepeat: "no-repeat",
|
|
641
|
-
maskPosition: "center",
|
|
642
|
-
WebkitMaskPosition: "center",
|
|
643
|
-
}}
|
|
644
|
-
/>
|
|
645
|
-
) : (
|
|
646
|
-
<Text fg={color}>{tool.glyph}</Text>
|
|
647
|
-
)}
|
|
648
|
-
</Box>
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
/** Blurs a focused text field, the focus change a consumed mousedown prevents. */
|
|
654
|
-
function releaseEditableFocus(event: ChartMouseEvent): void {
|
|
655
|
-
if (!event.target?.closest?.(`[data-gloom-role="${COMPOSITE_PANEL_ROLE}"]`)) return;
|
|
656
|
-
const active = (globalThis as {
|
|
657
|
-
document?: { activeElement?: { tagName?: string; blur?: () => void } };
|
|
658
|
-
}).document?.activeElement;
|
|
659
|
-
const tag = active?.tagName?.toUpperCase();
|
|
660
|
-
if (tag !== "INPUT" && tag !== "TEXTAREA") return;
|
|
661
|
-
active?.blur?.();
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function svgMaskUrl(svg: string): string {
|
|
665
|
-
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function ChartColorSwatch({
|
|
669
|
-
color,
|
|
670
|
-
active,
|
|
671
|
-
isDesktopWeb,
|
|
672
|
-
onPress,
|
|
673
|
-
}: {
|
|
674
|
-
color: string;
|
|
675
|
-
active: boolean;
|
|
676
|
-
isDesktopWeb: boolean;
|
|
677
|
-
onPress: () => void;
|
|
678
|
-
}) {
|
|
679
|
-
return (
|
|
680
|
-
<Box
|
|
681
|
-
flexDirection="row"
|
|
682
|
-
alignItems="center"
|
|
683
|
-
justifyContent="center"
|
|
684
|
-
width={2}
|
|
685
|
-
height={1}
|
|
686
|
-
flexShrink={0}
|
|
687
|
-
backgroundColor={active ? themeColors.selected : undefined}
|
|
688
|
-
hoverBackgroundColor={hoverBg()}
|
|
689
|
-
onMouseDown={(event: ChartMouseEvent) => {
|
|
690
|
-
consumeChartMouseEvent(event);
|
|
691
|
-
onPress();
|
|
692
|
-
}}
|
|
693
|
-
cursor="pointer"
|
|
694
|
-
data-gloom-interactive="true"
|
|
695
|
-
data-gloom-role="composite-chart-color"
|
|
696
|
-
data-gloom-label={`Drawing colour ${color}`}
|
|
697
|
-
data-active={active ? "true" : "false"}
|
|
698
|
-
style={isDesktopWeb
|
|
699
|
-
? {
|
|
700
|
-
width: 14,
|
|
701
|
-
height: 14,
|
|
702
|
-
borderRadius: 999,
|
|
703
|
-
backgroundColor: color,
|
|
704
|
-
border: `1.5px solid ${active ? themeColors.text : "transparent"}`,
|
|
705
|
-
}
|
|
706
|
-
: undefined}
|
|
707
|
-
>
|
|
708
|
-
{isDesktopWeb ? null : <Text fg={color}>{active ? "\u25c9" : "\u25cf"}</Text>}
|
|
709
|
-
</Box>
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function ChartToolbar({
|
|
714
|
-
armedTool,
|
|
715
|
-
isDesktopWeb,
|
|
716
|
-
left,
|
|
717
|
-
top,
|
|
718
|
-
drawColor,
|
|
719
|
-
showColors,
|
|
720
|
-
levelTool,
|
|
721
|
-
onArmTool,
|
|
722
|
-
onPickColor,
|
|
723
|
-
}: {
|
|
724
|
-
armedTool: ChartToolKind | null;
|
|
725
|
-
isDesktopWeb: boolean;
|
|
726
|
-
left: number;
|
|
727
|
-
top: number;
|
|
728
|
-
drawColor: string;
|
|
729
|
-
showColors: boolean;
|
|
730
|
-
/** The chart's levels can be edited here. */
|
|
731
|
-
levelTool: boolean;
|
|
732
|
-
onArmTool: (tool: ChartToolKind | null) => void;
|
|
733
|
-
onPickColor: (color: string) => void;
|
|
734
|
-
}) {
|
|
735
|
-
return (
|
|
736
|
-
<Box
|
|
737
|
-
position="absolute"
|
|
738
|
-
left={left}
|
|
739
|
-
top={top}
|
|
740
|
-
height={1}
|
|
741
|
-
flexDirection="row"
|
|
742
|
-
gap={1}
|
|
743
|
-
zIndex={30}
|
|
744
|
-
backgroundColor={themeColors.bg}
|
|
745
|
-
style={isDesktopWeb
|
|
746
|
-
? {
|
|
747
|
-
gap: 3,
|
|
748
|
-
padding: 3,
|
|
749
|
-
borderRadius: 7,
|
|
750
|
-
backgroundColor: `color-mix(in srgb, ${themeColors.bg} 78%, transparent)`,
|
|
751
|
-
backdropFilter: "blur(6px)",
|
|
752
|
-
width: "auto",
|
|
753
|
-
height: "auto",
|
|
754
|
-
}
|
|
755
|
-
: undefined}
|
|
756
|
-
data-gloom-role="composite-chart-toolbar"
|
|
757
|
-
>
|
|
758
|
-
{CHART_TOOLS.filter((tool) => levelTool || tool.kind !== "level").map((tool) => (
|
|
759
|
-
<ChartToolChip
|
|
760
|
-
key={tool.kind ?? "pan"}
|
|
761
|
-
tool={tool}
|
|
762
|
-
active={armedTool === tool.kind}
|
|
763
|
-
isDesktopWeb={isDesktopWeb}
|
|
764
|
-
onPress={() => onArmTool(tool.kind)}
|
|
765
|
-
/>
|
|
766
|
-
))}
|
|
767
|
-
{/* Colours only take space while something can use them. */}
|
|
768
|
-
{showColors ? CHART_DRAWING_COLORS.map((color) => (
|
|
769
|
-
<ChartColorSwatch
|
|
770
|
-
key={color}
|
|
771
|
-
color={color}
|
|
772
|
-
active={color === drawColor}
|
|
773
|
-
isDesktopWeb={isDesktopWeb}
|
|
774
|
-
onPress={() => onPickColor(color)}
|
|
775
|
-
/>
|
|
776
|
-
)) : null}
|
|
777
|
-
</Box>
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function resolveSeriesCursorYRatio(
|
|
782
|
-
panel: CompositePanelScene,
|
|
783
|
-
scene: CompositeChartScene,
|
|
784
|
-
): number | null {
|
|
785
|
-
for (const series of panel.series) {
|
|
786
|
-
const value = scene.cursorValues.find(
|
|
787
|
-
(entry) => entry.seriesId === series.source.id,
|
|
788
|
-
)?.value ?? null;
|
|
789
|
-
const domain = panel.axes[series.source.axis];
|
|
790
|
-
if (value === null || !domain) continue;
|
|
791
|
-
const yRatio = projectCompositeValue(value, domain);
|
|
792
|
-
if (yRatio !== null) return yRatio;
|
|
793
|
-
}
|
|
794
|
-
return null;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
interface CompositePanelSurfaceProps {
|
|
798
|
-
panel: CompositePanelScene;
|
|
799
|
-
scene: CompositeChartScene;
|
|
800
|
-
plotWidth: number;
|
|
801
|
-
leftAxisWidth: number;
|
|
802
|
-
rightAxisWidth: number;
|
|
803
|
-
axisGap: number;
|
|
804
|
-
colors: CompositeChartColors;
|
|
805
|
-
interactive: boolean;
|
|
806
|
-
/** False keeps the hover cursor but removes pan, zoom, and the tools. */
|
|
807
|
-
navigable: boolean;
|
|
808
|
-
formatAxisValue?: CompositeAxisValueFormatter;
|
|
809
|
-
remoteKind?: string;
|
|
810
|
-
viewport: CompositeViewportRange;
|
|
811
|
-
frame: CompositeNavigationFrame;
|
|
812
|
-
armedTool: ChartToolKind | null;
|
|
813
|
-
drawings: readonly ChartDrawing[];
|
|
814
|
-
selectedDrawingId: string | null;
|
|
815
|
-
drawColor: string;
|
|
816
|
-
onDraw: (drawing: ChartDrawing) => void;
|
|
817
|
-
onEditDrawing: (id: string, update: (drawing: ChartDrawing) => ChartDrawing) => void;
|
|
818
|
-
onSelectDrawing: (id: string | null) => void;
|
|
819
|
-
onActivate?: () => void;
|
|
820
|
-
onCursorDateChange: (date: Date | null) => void;
|
|
821
|
-
/** Positive positions move toward older observations. A gesture supplies the frame and origin it started from. */
|
|
822
|
-
onPanViewport: (
|
|
823
|
-
deltaPositions: number,
|
|
824
|
-
gesture?: { frame: CompositeNavigationFrame; from: CompositeViewportRange },
|
|
825
|
-
) => void;
|
|
826
|
-
onZoomViewport: (zoomFactor: number, anchorRatio: number) => void;
|
|
827
|
-
onSetViewport: (range: CompositeViewportRange) => void;
|
|
828
|
-
onToolSpanChange: (span: ChartToolSpan | null) => void;
|
|
829
|
-
/** The tool being placed from the keyboard, when it lives in this panel. */
|
|
830
|
-
keyboardToolDrag: KeyboardToolDrag | null;
|
|
831
|
-
/** A press on the plot: the pointer takes over from a keyboard placement. */
|
|
832
|
-
onPointerPress: () => void;
|
|
833
|
-
showTextFallback: boolean;
|
|
834
|
-
/** Set while the owner is picking a bar: a press picks instead of panning. */
|
|
835
|
-
onPickTime?: (date: Date) => void;
|
|
836
|
-
/** The chart's levels when they draw on this panel. */
|
|
837
|
-
levels: CompositeChartLevels | null;
|
|
838
|
-
selectedLevelId: string | null;
|
|
839
|
-
onSelectLevel: (id: string | null) => void;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function CompositePanelSurface({
|
|
843
|
-
panel,
|
|
844
|
-
scene,
|
|
845
|
-
plotWidth,
|
|
846
|
-
leftAxisWidth,
|
|
847
|
-
rightAxisWidth,
|
|
848
|
-
axisGap,
|
|
849
|
-
colors,
|
|
850
|
-
interactive,
|
|
851
|
-
navigable,
|
|
852
|
-
formatAxisValue,
|
|
853
|
-
remoteKind,
|
|
854
|
-
viewport,
|
|
855
|
-
frame,
|
|
856
|
-
armedTool,
|
|
857
|
-
drawings,
|
|
858
|
-
selectedDrawingId,
|
|
859
|
-
drawColor,
|
|
860
|
-
onDraw,
|
|
861
|
-
onEditDrawing,
|
|
862
|
-
onSelectDrawing,
|
|
863
|
-
onActivate,
|
|
864
|
-
onCursorDateChange,
|
|
865
|
-
onPanViewport,
|
|
866
|
-
onZoomViewport,
|
|
867
|
-
onSetViewport,
|
|
868
|
-
onToolSpanChange,
|
|
869
|
-
keyboardToolDrag,
|
|
870
|
-
onPointerPress,
|
|
871
|
-
showTextFallback,
|
|
872
|
-
onPickTime,
|
|
873
|
-
levels,
|
|
874
|
-
selectedLevelId,
|
|
875
|
-
onSelectLevel,
|
|
876
|
-
}: CompositePanelSurfaceProps) {
|
|
877
|
-
const isDesktopWeb = useUiCapabilities().nativePaneChrome === true;
|
|
878
|
-
const { cellHeightPx = 18, cellWidthPx = 8, fractionalViewport = false } = useUiCapabilities();
|
|
879
|
-
const renderer = useNativeRenderer();
|
|
880
|
-
const plotRef = useRef<BoxRenderable | null>(null);
|
|
881
|
-
const [cursorYRatio, setCursorYRatio] = useState<number | null>(null);
|
|
882
|
-
const [toolDrag, setToolDrag] = useState<ChartToolDrag | null>(null);
|
|
883
|
-
// A pointer drag owns the plot while it lasts; otherwise a tool placed from
|
|
884
|
-
// the keyboard draws and reads out exactly like one.
|
|
885
|
-
const keyboardDrag = toolDrag ? null : keyboardToolDrag;
|
|
886
|
-
const activeDrag = toolDrag ?? keyboardDrag?.drag ?? null;
|
|
887
|
-
// A keyboard placement's end sits at the level the user put it, so it
|
|
888
|
-
// draws a level line where otherwise only the pointer would.
|
|
889
|
-
const heldCursorYRatio = keyboardDrag ? keyboardDrag.drag.endYRatio : cursorYRatio;
|
|
890
|
-
const seriesCursorYRatio = useMemo(
|
|
891
|
-
() => resolveSeriesCursorYRatio(panel, scene),
|
|
892
|
-
[panel, scene],
|
|
893
|
-
);
|
|
894
|
-
const activeCursorYRatio = scene.cursorXRatio === null
|
|
895
|
-
? null
|
|
896
|
-
: heldCursorYRatio ?? seriesCursorYRatio;
|
|
897
|
-
const dragRef = useRef<
|
|
898
|
-
| PanGesture
|
|
899
|
-
| {
|
|
900
|
-
kind: "edit";
|
|
901
|
-
drawingId: string;
|
|
902
|
-
pointIndex: number | null;
|
|
903
|
-
lastXRatio: number;
|
|
904
|
-
lastYRatio: number;
|
|
905
|
-
}
|
|
906
|
-
| { kind: "level-move"; id: string }
|
|
907
|
-
| ChartToolDrag
|
|
908
|
-
| null
|
|
909
|
-
>(null);
|
|
910
|
-
const plotAspect = (plotWidth * cellWidthPx) / Math.max(panel.height * cellHeightPx, 1);
|
|
911
|
-
// The plot is wider than the viewport by the reserved right offset. Gestures
|
|
912
|
-
// read the pointer in plot space, so they convert through this.
|
|
913
|
-
const plotSpanFactor = 1 + compositeRightOffsetRatio(scene.timeScale);
|
|
914
|
-
const bitmapSize = useStaticChartBitmapSize(plotWidth, panel.height);
|
|
915
|
-
const bitmap = useCompositePanelBitmap({ panel, bitmapSize, colors, isDesktopWeb });
|
|
916
|
-
const columnLayout = useMemo(() => buildCompositeColumnLayout(panel), [panel]);
|
|
917
|
-
// The level line follows the pointer only. A keyboard or shared cursor knows
|
|
918
|
-
// its column, and the series markers and axis readout already say the value.
|
|
919
|
-
// A tool placed from the keyboard is the exception: its end is a level.
|
|
920
|
-
const pointerCursorYRatio = scene.cursorXRatio === null ? null : heldCursorYRatio;
|
|
921
|
-
const crosshair = useMemo(
|
|
922
|
-
() => resolvePanelCrosshair(panel, columnLayout, bitmap, scene.cursorXRatio, pointerCursorYRatio, colors.crosshair),
|
|
923
|
-
[bitmap, colors.crosshair, columnLayout, panel, pointerCursorYRatio, scene.cursorXRatio],
|
|
924
|
-
);
|
|
925
|
-
const measureDomain = useMemo(() => resolveMeasureAxisDomain(panel), [panel]);
|
|
926
|
-
const toolReadout = useMemo(() => {
|
|
927
|
-
if (!activeDrag) return null;
|
|
928
|
-
// A keyboard placement knows its data points exactly, even off screen; a
|
|
929
|
-
// pointer drag reads them back from the plot.
|
|
930
|
-
const startTime = keyboardDrag?.start.time
|
|
931
|
-
?? unprojectCompositeTimestamp(scene.timeScale, activeDrag.startXRatio);
|
|
932
|
-
const endTime = keyboardDrag?.end.time
|
|
933
|
-
?? unprojectCompositeTimestamp(scene.timeScale, activeDrag.endXRatio);
|
|
934
|
-
if (activeDrag.kind === "zoom") {
|
|
935
|
-
return {
|
|
936
|
-
direction: "up" as const,
|
|
937
|
-
summary: keyboardDrag
|
|
938
|
-
? summarizeZoomRange(scene, startTime, endTime)
|
|
939
|
-
: summarizeZoomSelection(scene, activeDrag),
|
|
940
|
-
startTime,
|
|
941
|
-
startValueLabel: null,
|
|
942
|
-
endValueLabel: null,
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
const startValue = keyboardDrag
|
|
946
|
-
? keyboardDrag.start.value
|
|
947
|
-
: measureDomain ? unprojectCompositeValue(activeDrag.startYRatio, measureDomain) : null;
|
|
948
|
-
const endValue = keyboardDrag
|
|
949
|
-
? keyboardDrag.end.value
|
|
950
|
-
: measureDomain ? unprojectCompositeValue(activeDrag.endYRatio, measureDomain) : null;
|
|
951
|
-
return {
|
|
952
|
-
direction: resolveMeasureDirection(startValue, endValue),
|
|
953
|
-
summary: summarizeMeasure({
|
|
954
|
-
startValue,
|
|
955
|
-
endValue,
|
|
956
|
-
startTime,
|
|
957
|
-
endTime,
|
|
958
|
-
// Cross-market cursor stops include every listing's observations;
|
|
959
|
-
// measured bars still follow the primary market's session scale.
|
|
960
|
-
bars: countMeasureBars(scene.timeScale.kind === "market"
|
|
961
|
-
? scene.timeScale.anchors.map(({ timestamp }) => new Date(timestamp))
|
|
962
|
-
: scene.dates, startTime, endTime),
|
|
963
|
-
domain: measureDomain,
|
|
964
|
-
}),
|
|
965
|
-
startTime,
|
|
966
|
-
startValueLabel: measureDomain && startValue !== null
|
|
967
|
-
? formatCompositeCursorValue(startValue, measureDomain)
|
|
968
|
-
: null,
|
|
969
|
-
endValueLabel: measureDomain && endValue !== null
|
|
970
|
-
? formatCompositeCursorValue(endValue, measureDomain)
|
|
971
|
-
: null,
|
|
972
|
-
};
|
|
973
|
-
}, [activeDrag, keyboardDrag, measureDomain, scene]);
|
|
974
|
-
const toolSummary = toolReadout?.summary ?? null;
|
|
975
|
-
const toolStartTime = toolReadout?.startTime ?? null;
|
|
976
|
-
const toolSpan = useMemo(() => {
|
|
977
|
-
if (!activeDrag || !toolSummary || toolStartTime === null) return null;
|
|
978
|
-
return {
|
|
979
|
-
startXRatio: activeDrag.startXRatio,
|
|
980
|
-
endXRatio: activeDrag.endXRatio,
|
|
981
|
-
startTime: toolStartTime,
|
|
982
|
-
color: activeDrag.kind === "zoom"
|
|
983
|
-
? colors.crosshair
|
|
984
|
-
: toolReadout?.direction === "down" ? colors.negative : themeColors.positive,
|
|
985
|
-
};
|
|
986
|
-
}, [activeDrag, colors.crosshair, colors.negative, toolReadout?.direction, toolStartTime, toolSummary]);
|
|
987
|
-
useEffect(() => {
|
|
988
|
-
onToolSpanChange(toolSpan);
|
|
989
|
-
}, [onToolSpanChange, toolSpan]);
|
|
990
|
-
const panelDrawings = useMemo(
|
|
991
|
-
() => drawings.filter((drawing) => drawing.panelId === panel.id),
|
|
992
|
-
[drawings, panel.id],
|
|
993
|
-
);
|
|
994
|
-
// A dragged level moves here and is saved once, when the pointer lets go.
|
|
995
|
-
const [levelDraft, setLevelDraft] = useState<{ id: string; value: number } | null>(null);
|
|
996
|
-
const levelDraftRef = useRef(levelDraft);
|
|
997
|
-
levelDraftRef.current = levelDraft;
|
|
998
|
-
const levelDomain = useMemo(() => {
|
|
999
|
-
const series = levels ? panel.series.find((entry) => entry.source.id === levels.seriesId) : undefined;
|
|
1000
|
-
return series ? panel.axes[series.source.axis] ?? null : null;
|
|
1001
|
-
}, [levels, panel]);
|
|
1002
|
-
const projectedLevels = useMemo(
|
|
1003
|
-
() => levels && levelDomain ? projectLevels(levels.items, levelDomain, levelDraft) : NO_LEVELS,
|
|
1004
|
-
[levelDomain, levelDraft, levels],
|
|
1005
|
-
);
|
|
1006
|
-
const bitmapLayers = useMemo(() => {
|
|
1007
|
-
if (!bitmap) return null;
|
|
1008
|
-
// The desktop composites overlays as vectors, so the plot raster stays put
|
|
1009
|
-
// while a tool drags. Copying and reblending it per frame is what made the
|
|
1010
|
-
// ruler feel heavy.
|
|
1011
|
-
if (isDesktopWeb) return [bitmap];
|
|
1012
|
-
const base = !activeDrag && panelDrawings.length === 0 ? bitmap : drawChartToolOverlay(
|
|
1013
|
-
bitmap,
|
|
1014
|
-
activeDrag,
|
|
1015
|
-
{
|
|
1016
|
-
positive: themeColors.positive,
|
|
1017
|
-
negative: colors.negative,
|
|
1018
|
-
zoom: colors.crosshair,
|
|
1019
|
-
draw: drawColor,
|
|
1020
|
-
},
|
|
1021
|
-
toolReadout?.direction ?? "up",
|
|
1022
|
-
{ scene, panel, items: panelDrawings, selectedId: selectedDrawingId },
|
|
1023
|
-
);
|
|
1024
|
-
return [projectedLevels.length > 0 ? paintLevels(base, projectedLevels, selectedLevelId) : base];
|
|
1025
|
-
}, [
|
|
1026
|
-
activeDrag,
|
|
1027
|
-
bitmap,
|
|
1028
|
-
colors.crosshair,
|
|
1029
|
-
colors.negative,
|
|
1030
|
-
drawColor,
|
|
1031
|
-
isDesktopWeb,
|
|
1032
|
-
panel,
|
|
1033
|
-
panelDrawings,
|
|
1034
|
-
projectedLevels,
|
|
1035
|
-
scene,
|
|
1036
|
-
selectedDrawingId,
|
|
1037
|
-
selectedLevelId,
|
|
1038
|
-
toolReadout?.direction,
|
|
1039
|
-
]);
|
|
1040
|
-
const vectors = useMemo<ChartSurfaceProps["vectors"]>(() => {
|
|
1041
|
-
if (!isDesktopWeb) return null;
|
|
1042
|
-
const shapes = [...levelVectors(projectedLevels, selectedLevelId), ...buildChartToolVectors({
|
|
1043
|
-
scene,
|
|
1044
|
-
panel,
|
|
1045
|
-
drawings: panelDrawings,
|
|
1046
|
-
selectedId: selectedDrawingId,
|
|
1047
|
-
drag: activeDrag,
|
|
1048
|
-
colors: {
|
|
1049
|
-
positive: themeColors.positive,
|
|
1050
|
-
negative: colors.negative,
|
|
1051
|
-
zoom: colors.crosshair,
|
|
1052
|
-
draw: drawColor,
|
|
1053
|
-
},
|
|
1054
|
-
direction: toolReadout?.direction ?? "up",
|
|
1055
|
-
})];
|
|
1056
|
-
return shapes.length > 0 ? shapes : null;
|
|
1057
|
-
}, [
|
|
1058
|
-
activeDrag,
|
|
1059
|
-
colors.crosshair,
|
|
1060
|
-
colors.negative,
|
|
1061
|
-
drawColor,
|
|
1062
|
-
isDesktopWeb,
|
|
1063
|
-
panel,
|
|
1064
|
-
panelDrawings,
|
|
1065
|
-
projectedLevels,
|
|
1066
|
-
scene,
|
|
1067
|
-
selectedDrawingId,
|
|
1068
|
-
selectedLevelId,
|
|
1069
|
-
toolReadout?.direction,
|
|
1070
|
-
]);
|
|
1071
|
-
const textLines = useMemo(
|
|
1072
|
-
() => isDesktopWeb || !showTextFallback
|
|
1073
|
-
? []
|
|
1074
|
-
: writeLevelText(
|
|
1075
|
-
renderCompositePanelText(panel, plotWidth, scene.cursorXRatio, pointerCursorYRatio),
|
|
1076
|
-
projectedLevels,
|
|
1077
|
-
selectedLevelId,
|
|
1078
|
-
),
|
|
1079
|
-
[isDesktopWeb, panel, plotWidth, pointerCursorYRatio, projectedLevels, scene.cursorXRatio, selectedLevelId, showTextFallback],
|
|
1080
|
-
);
|
|
1081
|
-
const leftAxisLabels = useMemo(
|
|
1082
|
-
() => axisLabelRows(
|
|
1083
|
-
renderCompositeAxisText(panel.axes.left, panel.height, leftAxisWidth, "left", formatAxisValue),
|
|
1084
|
-
),
|
|
1085
|
-
[formatAxisValue, leftAxisWidth, panel],
|
|
1086
|
-
);
|
|
1087
|
-
const rightAxisLabels = useMemo(
|
|
1088
|
-
() => axisLabelRows(
|
|
1089
|
-
renderCompositeAxisText(panel.axes.right, panel.height, rightAxisWidth, "right", formatAxisValue),
|
|
1090
|
-
),
|
|
1091
|
-
[formatAxisValue, panel, rightAxisWidth],
|
|
1092
|
-
);
|
|
1093
|
-
const leftAxisTicks = useMemo(
|
|
1094
|
-
() => compositeAxisTickLabels(panel.axes.left, leftAxisWidth, formatAxisValue),
|
|
1095
|
-
[formatAxisValue, leftAxisWidth, panel],
|
|
1096
|
-
);
|
|
1097
|
-
const rightAxisTicks = useMemo(
|
|
1098
|
-
() => compositeAxisTickLabels(panel.axes.right, rightAxisWidth, formatAxisValue),
|
|
1099
|
-
[formatAxisValue, panel, rightAxisWidth],
|
|
1100
|
-
);
|
|
1101
|
-
const cursorRow = activeCursorYRatio === null
|
|
1102
|
-
? null
|
|
1103
|
-
: Math.round(activeCursorYRatio * Math.max(panel.height - 1, 0));
|
|
1104
|
-
const cursorPixelY = activeCursorYRatio === null
|
|
1105
|
-
? null
|
|
1106
|
-
: activeCursorYRatio * Math.max(panel.height * cellHeightPx - 1, 0);
|
|
1107
|
-
const leftCursorLabel = cursorAxisLabel(panel, "left", activeCursorYRatio, formatAxisValue);
|
|
1108
|
-
const rightCursorLabel = cursorAxisLabel(panel, "right", activeCursorYRatio, formatAxisValue);
|
|
1109
|
-
const measureReadout = useMemo(() => {
|
|
1110
|
-
if (!activeDrag || !toolReadout?.summary) return null;
|
|
1111
|
-
const text = toolReadout.summary;
|
|
1112
|
-
const centreX = (activeDrag.startXRatio + activeDrag.endXRatio) / 2;
|
|
1113
|
-
const centreY = activeDrag.kind === "zoom"
|
|
1114
|
-
? 0.5
|
|
1115
|
-
: (activeDrag.startYRatio + activeDrag.endYRatio) / 2;
|
|
1116
|
-
const width = Math.min([...text].length, plotWidth);
|
|
1117
|
-
return {
|
|
1118
|
-
text,
|
|
1119
|
-
width,
|
|
1120
|
-
color: activeDrag.kind === "zoom"
|
|
1121
|
-
? colors.text
|
|
1122
|
-
: toolReadout.direction === "down" ? colors.negative : themeColors.positive,
|
|
1123
|
-
left: leftAxisWidth + (leftAxisWidth ? axisGap : 0)
|
|
1124
|
-
+ Math.max(0, Math.min(plotWidth - width, Math.round(centreX * plotWidth - width / 2))),
|
|
1125
|
-
top: Math.max(0, Math.min(panel.height - 1, Math.round(centreY * (panel.height - 1)))),
|
|
1126
|
-
};
|
|
1127
|
-
}, [
|
|
1128
|
-
activeDrag,
|
|
1129
|
-
axisGap,
|
|
1130
|
-
colors.negative,
|
|
1131
|
-
colors.text,
|
|
1132
|
-
leftAxisWidth,
|
|
1133
|
-
panel.height,
|
|
1134
|
-
plotWidth,
|
|
1135
|
-
toolReadout,
|
|
1136
|
-
]);
|
|
1137
|
-
const axisMarkers = useMemo(() => {
|
|
1138
|
-
// The crosshair already marks the moving end, so the axes only need the
|
|
1139
|
-
// anchor the drag started from.
|
|
1140
|
-
if (!activeDrag || activeDrag.kind === "zoom" || !toolReadout?.startValueLabel) return null;
|
|
1141
|
-
return {
|
|
1142
|
-
side: null,
|
|
1143
|
-
// A keyboard anchor can leave the value range once the view moves.
|
|
1144
|
-
yRatio: Math.max(0, Math.min(1, activeDrag.startYRatio)),
|
|
1145
|
-
label: toolReadout.startValueLabel,
|
|
1146
|
-
color: toolReadout.direction === "down" ? colors.negative : themeColors.positive,
|
|
1147
|
-
};
|
|
1148
|
-
}, [activeDrag, colors.negative, toolReadout]);
|
|
1149
|
-
const lastPriceMarker = useMemo(() => {
|
|
1150
|
-
const marker = panel.lastPrice;
|
|
1151
|
-
const domain = marker ? panel.axes[marker.axis] : undefined;
|
|
1152
|
-
if (!marker || !domain) return null;
|
|
1153
|
-
return {
|
|
1154
|
-
side: marker.axis,
|
|
1155
|
-
yRatio: marker.yRatio,
|
|
1156
|
-
label: formatAxisValue
|
|
1157
|
-
? formatAxisValue(marker.value, domain)
|
|
1158
|
-
: formatCompositeCursorValue(marker.value, domain),
|
|
1159
|
-
color: marker.color,
|
|
1160
|
-
};
|
|
1161
|
-
}, [formatAxisValue, panel]);
|
|
1162
|
-
const priorCloseMarker = useMemo(() => {
|
|
1163
|
-
const marker = panel.priorClose;
|
|
1164
|
-
const domain = marker ? panel.axes[marker.axis] : undefined;
|
|
1165
|
-
if (!marker || !domain) return null;
|
|
1166
|
-
return {
|
|
1167
|
-
side: marker.axis,
|
|
1168
|
-
yRatio: marker.yRatio,
|
|
1169
|
-
label: formatAxisValue ? formatAxisValue(marker.value, domain) : formatCompositeCursorValue(marker.value, domain),
|
|
1170
|
-
color: colors.textDim,
|
|
1171
|
-
};
|
|
1172
|
-
}, [colors.textDim, formatAxisValue, panel]);
|
|
1173
|
-
const levelMarkers = useMemo(() => levelDomain ? [...projectedLevels]
|
|
1174
|
-
// The picked level's label first, then alerts', when they compete for room.
|
|
1175
|
-
.sort((left, right) => Number(right.level.id === selectedLevelId) - Number(left.level.id === selectedLevelId)
|
|
1176
|
-
|| Number(left.level.editable) - Number(right.level.editable))
|
|
1177
|
-
.map(({ level, yRatio }) => ({
|
|
1178
|
-
side: levelDomain.side,
|
|
1179
|
-
yRatio,
|
|
1180
|
-
label: formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain),
|
|
1181
|
-
color: level.color,
|
|
1182
|
-
})) : [], [formatAxisValue, levelDomain, projectedLevels, selectedLevelId]);
|
|
1183
|
-
const buildAxisMarkers = (side: "left" | "right") => {
|
|
1184
|
-
if (!panel.axes[side]) return undefined;
|
|
1185
|
-
const toAxisMarker = (marker: { yRatio: number; label: string; color: string }) => ({
|
|
1186
|
-
row: Math.round(marker.yRatio * Math.max(panel.height - 1, 0)),
|
|
1187
|
-
pixelY: marker.yRatio * Math.max(panel.height * cellHeightPx - 1, 0),
|
|
1188
|
-
label: marker.label,
|
|
1189
|
-
color: marker.color,
|
|
1190
|
-
});
|
|
1191
|
-
const onSide = <T extends { side: string | null }>(marker: T | null): marker is T => (
|
|
1192
|
-
!!marker && (marker.side === null || marker.side === side)
|
|
1193
|
-
);
|
|
1194
|
-
const fixed = [lastPriceMarker, axisMarkers].filter(onSide).map(toAxisMarker);
|
|
1195
|
-
// The prior close's label yields to every level's.
|
|
1196
|
-
const levelLabels = [...levelMarkers, priorCloseMarker].filter(onSide).map(toAxisMarker);
|
|
1197
|
-
// A label is a row tall, so close levels would stack into an unreadable
|
|
1198
|
-
// pile and hide the last price: each keeps its line, and only the labels
|
|
1199
|
-
// with a row to themselves show. Desktop badges sit between rows.
|
|
1200
|
-
const markers = [
|
|
1201
|
-
...fitAxisLabels(levelLabels, fixed, (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
|
|
1202
|
-
...fixed,
|
|
1203
|
-
];
|
|
1204
|
-
return markers.length > 0 ? markers : undefined;
|
|
1205
|
-
};
|
|
1206
|
-
const leftAxisMarkers = buildAxisMarkers("left");
|
|
1207
|
-
const rightAxisMarkers = buildAxisMarkers("right");
|
|
1208
|
-
|
|
1209
|
-
const pointerRatios = useCallback((event: ChartMouseEvent) => {
|
|
1210
|
-
const pointerTarget = plotRef.current as unknown as Parameters<typeof getLocalPlotPointer>[1];
|
|
1211
|
-
const pointer = getLocalPlotPointer(event, pointerTarget, renderer);
|
|
1212
|
-
if (!pointer) return null;
|
|
1213
|
-
return {
|
|
1214
|
-
xRatio: plotWidth <= 1 ? 0 : Math.max(0, Math.min(1, pointer.cellX / (plotWidth - 1))),
|
|
1215
|
-
yRatio: panel.height <= 1 ? 0.5 : Math.max(0, Math.min(1, pointer.cellY / (panel.height - 1))),
|
|
1216
|
-
};
|
|
1217
|
-
}, [panel.height, plotWidth, renderer]);
|
|
1218
|
-
const updateCursor = useCallback((event: ChartMouseEvent): boolean => {
|
|
1219
|
-
const pointerTarget = plotRef.current as unknown as Parameters<typeof getLocalPlotPointer>[1];
|
|
1220
|
-
const pointer = getLocalPlotPointer(event, pointerTarget, renderer);
|
|
1221
|
-
if (!pointer) return false;
|
|
1222
|
-
const nextDate = resolveCompositeCursorDate(scene, pointer.cellX);
|
|
1223
|
-
if (!nextDate) return false;
|
|
1224
|
-
const nextYRatio = panel.height <= 1
|
|
1225
|
-
? 0.5
|
|
1226
|
-
: Math.max(0, Math.min(1, pointer.cellY / (panel.height - 1)));
|
|
1227
|
-
setCursorYRatio((current) => current === nextYRatio ? current : nextYRatio);
|
|
1228
|
-
onCursorDateChange(nextDate);
|
|
1229
|
-
consumeChartMouseEvent(event);
|
|
1230
|
-
return true;
|
|
1231
|
-
}, [onCursorDateChange, panel.height, renderer, scene]);
|
|
1232
|
-
const clearCursor = useCallback(() => {
|
|
1233
|
-
setCursorYRatio(null);
|
|
1234
|
-
onCursorDateChange(null);
|
|
1235
|
-
}, [onCursorDateChange]);
|
|
1236
|
-
const startDrag = useCallback((event: ChartMouseEvent) => {
|
|
1237
|
-
onActivate?.();
|
|
1238
|
-
onPointerPress();
|
|
1239
|
-
// The plot consumes the press, so the browser never moves focus off a text
|
|
1240
|
-
// field for us. Hand it back, but only for a press that truly landed here:
|
|
1241
|
-
// a dialog over the plot must keep the focus it just took.
|
|
1242
|
-
if (isDesktopWeb) releaseEditableFocus(event);
|
|
1243
|
-
consumeChartMouseEvent(event);
|
|
1244
|
-
if (onPickTime) {
|
|
1245
|
-
const pointer = getLocalPlotPointer(event, plotRef.current as unknown as Parameters<typeof getLocalPlotPointer>[1], renderer);
|
|
1246
|
-
const date = pointer ? resolveCompositeCursorDate(scene, pointer.cellX) : null;
|
|
1247
|
-
if (date) onPickTime(date);
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
// A keyboard-armed tool covers terminals that never forward modifier drags.
|
|
1251
|
-
const tool = resolveChartToolKind(event.modifiers) ?? armedTool;
|
|
1252
|
-
if (tool === "level" && levels?.onEdit && levelDomain) {
|
|
1253
|
-
const ratios = pointerRatios(event);
|
|
1254
|
-
if (!ratios) return;
|
|
1255
|
-
const hit = hitTestLevel(projectedLevels, ratios.yRatio, levelGrabRatio(panel.height));
|
|
1256
|
-
if (hit) {
|
|
1257
|
-
onSelectLevel(hit.id);
|
|
1258
|
-
if (hit.editable) dragRef.current = { kind: "level-move", id: hit.id };
|
|
1259
|
-
} else {
|
|
1260
|
-
const value = unprojectCompositeValue(ratios.yRatio, levelDomain);
|
|
1261
|
-
if (value !== null) {
|
|
1262
|
-
const id = nextLevelId();
|
|
1263
|
-
levels.onEdit({ kind: "add", id, value: roundLevelValue(value, levelDomain) });
|
|
1264
|
-
onSelectLevel(id);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
updateCursor(event);
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
if (tool) {
|
|
1271
|
-
const ratios = pointerRatios(event);
|
|
1272
|
-
if (!ratios) return;
|
|
1273
|
-
// With a drawing tool in hand, grabbing an existing shape edits it
|
|
1274
|
-
// instead of starting a new one on top of it.
|
|
1275
|
-
const hit = isDrawingTool(tool)
|
|
1276
|
-
? hitTestDrawings(drawings, scene, panel, ratios, plotAspect)
|
|
1277
|
-
: null;
|
|
1278
|
-
if (hit) {
|
|
1279
|
-
onSelectDrawing(hit.drawing.id);
|
|
1280
|
-
dragRef.current = {
|
|
1281
|
-
kind: "edit",
|
|
1282
|
-
drawingId: hit.drawing.id,
|
|
1283
|
-
pointIndex: hit.pointIndex,
|
|
1284
|
-
lastXRatio: ratios.xRatio,
|
|
1285
|
-
lastYRatio: ratios.yRatio,
|
|
1286
|
-
};
|
|
1287
|
-
updateCursor(event);
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
const started: ChartToolDrag = {
|
|
1291
|
-
kind: tool,
|
|
1292
|
-
startXRatio: ratios.xRatio,
|
|
1293
|
-
startYRatio: ratios.yRatio,
|
|
1294
|
-
endXRatio: ratios.xRatio,
|
|
1295
|
-
endYRatio: ratios.yRatio,
|
|
1296
|
-
path: [{ xRatio: ratios.xRatio, yRatio: ratios.yRatio }],
|
|
1297
|
-
};
|
|
1298
|
-
if (isDrawingTool(tool)) onSelectDrawing(null);
|
|
1299
|
-
dragRef.current = { ...started, path: [...started.path] };
|
|
1300
|
-
setToolDrag(started);
|
|
1301
|
-
updateCursor(event);
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
if (!updateCursor(event)) return;
|
|
1305
|
-
dragRef.current = startPanGesture(
|
|
1306
|
-
frame,
|
|
1307
|
-
viewport,
|
|
1308
|
-
plotWidth,
|
|
1309
|
-
getGlobalMouseX(event, renderer),
|
|
1310
|
-
plotSpanFactor,
|
|
1311
|
-
);
|
|
1312
|
-
}, [
|
|
1313
|
-
armedTool,
|
|
1314
|
-
drawings,
|
|
1315
|
-
frame,
|
|
1316
|
-
levelDomain,
|
|
1317
|
-
levels,
|
|
1318
|
-
onActivate,
|
|
1319
|
-
onPickTime,
|
|
1320
|
-
onPointerPress,
|
|
1321
|
-
onSelectDrawing,
|
|
1322
|
-
onSelectLevel,
|
|
1323
|
-
panel,
|
|
1324
|
-
plotAspect,
|
|
1325
|
-
projectedLevels,
|
|
1326
|
-
plotSpanFactor,
|
|
1327
|
-
plotWidth,
|
|
1328
|
-
pointerRatios,
|
|
1329
|
-
renderer,
|
|
1330
|
-
scene,
|
|
1331
|
-
updateCursor,
|
|
1332
|
-
viewport,
|
|
1333
|
-
]);
|
|
1334
|
-
const pressCursor = useCallback((event: ChartMouseEvent) => {
|
|
1335
|
-
onActivate?.();
|
|
1336
|
-
updateCursor(event);
|
|
1337
|
-
}, [onActivate, updateCursor]);
|
|
1338
|
-
const dragViewport = useCallback((event: ChartMouseEvent) => {
|
|
1339
|
-
const drag = dragRef.current;
|
|
1340
|
-
if (!drag) return;
|
|
1341
|
-
consumeChartMouseEvent(event);
|
|
1342
|
-
updateCursor(event);
|
|
1343
|
-
if (drag.kind === "level-move") {
|
|
1344
|
-
const ratios = pointerRatios(event);
|
|
1345
|
-
const value = ratios && levelDomain ? unprojectCompositeValue(ratios.yRatio, levelDomain) : null;
|
|
1346
|
-
if (value !== null && levelDomain) setLevelDraft({ id: drag.id, value: roundLevelValue(value, levelDomain) });
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
if (drag.kind === "edit") {
|
|
1350
|
-
const ratios = pointerRatios(event);
|
|
1351
|
-
if (!ratios) return;
|
|
1352
|
-
// Read the deltas before advancing the ref: the state updater runs later,
|
|
1353
|
-
// and a mutable capture would hand it a zero move every time.
|
|
1354
|
-
const deltaXRatio = ratios.xRatio - drag.lastXRatio;
|
|
1355
|
-
const deltaYRatio = ratios.yRatio - drag.lastYRatio;
|
|
1356
|
-
const { drawingId, pointIndex } = drag;
|
|
1357
|
-
drag.lastXRatio = ratios.xRatio;
|
|
1358
|
-
drag.lastYRatio = ratios.yRatio;
|
|
1359
|
-
onEditDrawing(
|
|
1360
|
-
drawingId,
|
|
1361
|
-
(drawing) => shiftDrawing(drawing, scene, panel, deltaXRatio, deltaYRatio, pointIndex),
|
|
1362
|
-
);
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
if (drag.kind !== "pan") {
|
|
1366
|
-
const ratios = pointerRatios(event);
|
|
1367
|
-
if (!ratios) return;
|
|
1368
|
-
drag.endXRatio = ratios.xRatio;
|
|
1369
|
-
drag.endYRatio = ratios.yRatio;
|
|
1370
|
-
if (drag.kind === "pencil") drag.path.push({ xRatio: ratios.xRatio, yRatio: ratios.yRatio });
|
|
1371
|
-
setToolDrag({ ...drag, path: [...drag.path] });
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
const globalX = getGlobalMouseX(event, renderer);
|
|
1375
|
-
if (drag.frame !== frame) {
|
|
1376
|
-
// Data refreshed mid-drag. The new frame maps positions differently, so
|
|
1377
|
-
// continue from where the plot is now instead of replaying the drag.
|
|
1378
|
-
Object.assign(drag, startPanGesture(frame, viewport, plotWidth, globalX, plotSpanFactor));
|
|
1379
|
-
}
|
|
1380
|
-
onPanViewport(
|
|
1381
|
-
(globalX - drag.startGlobalX) * drag.positionsPerCell,
|
|
1382
|
-
{ frame: drag.frame, from: drag.startViewport },
|
|
1383
|
-
);
|
|
1384
|
-
}, [
|
|
1385
|
-
frame,
|
|
1386
|
-
levelDomain,
|
|
1387
|
-
onEditDrawing,
|
|
1388
|
-
onPanViewport,
|
|
1389
|
-
panel,
|
|
1390
|
-
plotSpanFactor,
|
|
1391
|
-
plotWidth,
|
|
1392
|
-
pointerRatios,
|
|
1393
|
-
renderer,
|
|
1394
|
-
scene,
|
|
1395
|
-
updateCursor,
|
|
1396
|
-
viewport,
|
|
1397
|
-
]);
|
|
1398
|
-
const finishToolDrag = useCallback(() => {
|
|
1399
|
-
const drag = dragRef.current;
|
|
1400
|
-
if (!drag || drag.kind === "pan") return;
|
|
1401
|
-
dragRef.current = null;
|
|
1402
|
-
if (drag.kind === "edit") return;
|
|
1403
|
-
if (drag.kind === "level-move") {
|
|
1404
|
-
const draft = levelDraftRef.current;
|
|
1405
|
-
setLevelDraft(null);
|
|
1406
|
-
if (draft) levels?.onEdit?.({ kind: "move", id: draft.id, value: draft.value });
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
setToolDrag(null);
|
|
1410
|
-
if (isDrawingTool(drag.kind)) {
|
|
1411
|
-
const drawing = resolveDrawingFromDrag(scene, panel, drag, drawColor, nextDrawingId());
|
|
1412
|
-
if (drawing) onDraw(drawing);
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
if (drag.kind !== "zoom") return;
|
|
1416
|
-
const range = resolveZoomBoxRange(scene, drag, frame.minimumSpanMs);
|
|
1417
|
-
if (range) onSetViewport(range);
|
|
1418
|
-
}, [drawColor, frame.minimumSpanMs, levels, onDraw, onSetViewport, panel, scene]);
|
|
1419
|
-
const resetDrag = useCallback(() => {
|
|
1420
|
-
if (dragRef.current?.kind === "pan") {
|
|
1421
|
-
dragRef.current = null;
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
finishToolDrag();
|
|
1425
|
-
}, [finishToolDrag]);
|
|
1426
|
-
const handleMouseMove = useCallback((event: ChartMouseEvent) => {
|
|
1427
|
-
// A release over a neighbouring pane never reaches this surface, so treat a
|
|
1428
|
-
// plain move, which only fires with no button held, as the missing release.
|
|
1429
|
-
finishToolDrag();
|
|
1430
|
-
updateCursor(event);
|
|
1431
|
-
}, [finishToolDrag, updateCursor]);
|
|
1432
|
-
// Trackpads fire wheel events faster than the plot can paint, so one frame
|
|
1433
|
-
// absorbs every event that arrived since the last one.
|
|
1434
|
-
const pendingWheelRef = useRef<PendingWheel | null>(null);
|
|
1435
|
-
const wheelFrameRef = useRef<number | null>(null);
|
|
1436
|
-
const latestWheelStateRef = useRef({ frame, viewport, onPanViewport, onZoomViewport, plotSpanFactor });
|
|
1437
|
-
latestWheelStateRef.current = { frame, viewport, onPanViewport, onZoomViewport, plotSpanFactor };
|
|
1438
|
-
const flushWheel = useCallback(() => {
|
|
1439
|
-
wheelFrameRef.current = null;
|
|
1440
|
-
const pending = pendingWheelRef.current;
|
|
1441
|
-
pendingWheelRef.current = null;
|
|
1442
|
-
if (!pending) return;
|
|
1443
|
-
const latest = latestWheelStateRef.current;
|
|
1444
|
-
if (pending.panRatio !== 0) {
|
|
1445
|
-
const positions = compositeViewportPositions(latest.frame, latest.viewport);
|
|
1446
|
-
const span = positions ? Math.max(positions.end - positions.start, Number.EPSILON) : 0;
|
|
1447
|
-
latest.onPanViewport(pending.panRatio * span * latest.plotSpanFactor);
|
|
1448
|
-
}
|
|
1449
|
-
if (pending.zoomLog !== 0) {
|
|
1450
|
-
latest.onZoomViewport(Math.exp(pending.zoomLog), pending.anchorRatio);
|
|
1451
|
-
}
|
|
1452
|
-
}, []);
|
|
1453
|
-
useEffect(() => () => {
|
|
1454
|
-
pendingWheelRef.current = null;
|
|
1455
|
-
if (wheelFrameRef.current !== null) {
|
|
1456
|
-
webFrame.cancelAnimationFrame?.(wheelFrameRef.current);
|
|
1457
|
-
wheelFrameRef.current = null;
|
|
1458
|
-
}
|
|
1459
|
-
}, []);
|
|
1460
|
-
const panFromWheel = useCallback((event: ChartMouseEvent) => {
|
|
1461
|
-
const scroll = event.scroll;
|
|
1462
|
-
if (!scroll) return;
|
|
1463
|
-
onActivate?.();
|
|
1464
|
-
consumeChartMouseEvent(event);
|
|
1465
|
-
const pointerTarget = plotRef.current as unknown as Parameters<typeof getLocalPlotPointer>[1];
|
|
1466
|
-
const pointer = getLocalPlotPointer(event, pointerTarget, renderer);
|
|
1467
|
-
updateCursor(event);
|
|
1468
|
-
const pending = pendingWheelRef.current ?? { panRatio: 0, zoomLog: 0, anchorRatio: 0.5 };
|
|
1469
|
-
if (event.modifiers.ctrl && pointer && isVerticalWheelDirection(scroll.direction)) {
|
|
1470
|
-
pending.zoomLog += Math.log(resolveCompositeWheelZoom(scroll));
|
|
1471
|
-
// The anchor is a fraction of the viewport, so a pointer over the empty
|
|
1472
|
-
// right offset anchors on the newest observation instead of past it.
|
|
1473
|
-
pending.anchorRatio = Math.min(
|
|
1474
|
-
1,
|
|
1475
|
-
pointer.cellX / Math.max(plotWidth - 1, 1) * plotSpanFactor,
|
|
1476
|
-
);
|
|
1477
|
-
} else {
|
|
1478
|
-
pending.panRatio += resolveCompositeWheelPan(scroll, plotWidth * cellWidthPx);
|
|
1479
|
-
}
|
|
1480
|
-
pendingWheelRef.current = pending;
|
|
1481
|
-
if (!isDesktopWeb || typeof webFrame.requestAnimationFrame !== "function") {
|
|
1482
|
-
flushWheel();
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
if (wheelFrameRef.current === null) {
|
|
1486
|
-
wheelFrameRef.current = webFrame.requestAnimationFrame(flushWheel);
|
|
1487
|
-
}
|
|
1488
|
-
}, [
|
|
1489
|
-
cellWidthPx,
|
|
1490
|
-
flushWheel,
|
|
1491
|
-
isDesktopWeb,
|
|
1492
|
-
onActivate,
|
|
1493
|
-
plotSpanFactor,
|
|
1494
|
-
plotWidth,
|
|
1495
|
-
renderer,
|
|
1496
|
-
updateCursor,
|
|
1497
|
-
]);
|
|
1498
|
-
|
|
1499
|
-
return (
|
|
1500
|
-
<Box
|
|
1501
|
-
flexDirection="row"
|
|
1502
|
-
height={panel.height}
|
|
1503
|
-
width={plotWidth + leftAxisWidth + rightAxisWidth + axisGap * ((leftAxisWidth ? 1 : 0) + (rightAxisWidth ? 1 : 0))}
|
|
1504
|
-
position="relative"
|
|
1505
|
-
>
|
|
1506
|
-
{measureReadout ? (
|
|
1507
|
-
<Box
|
|
1508
|
-
position="absolute"
|
|
1509
|
-
left={measureReadout.left}
|
|
1510
|
-
top={measureReadout.top}
|
|
1511
|
-
width={measureReadout.width}
|
|
1512
|
-
height={1}
|
|
1513
|
-
zIndex={25}
|
|
1514
|
-
backgroundColor={colors.background}
|
|
1515
|
-
style={isDesktopWeb
|
|
1516
|
-
? {
|
|
1517
|
-
width: "auto",
|
|
1518
|
-
paddingInline: 4,
|
|
1519
|
-
borderRadius: 4,
|
|
1520
|
-
backgroundColor: `color-mix(in srgb, ${colors.background} 82%, transparent)`,
|
|
1521
|
-
}
|
|
1522
|
-
: undefined}
|
|
1523
|
-
data-gloom-role="composite-chart-measure"
|
|
1524
|
-
>
|
|
1525
|
-
<Text fg={measureReadout.color}>{measureReadout.text}</Text>
|
|
1526
|
-
</Box>
|
|
1527
|
-
) : null}
|
|
1528
|
-
{leftAxisWidth > 0 ? (
|
|
1529
|
-
<>
|
|
1530
|
-
<PriceAxisLabels
|
|
1531
|
-
axisLabels={leftAxisLabels}
|
|
1532
|
-
axisTicks={leftAxisTicks}
|
|
1533
|
-
axisWidth={leftAxisWidth}
|
|
1534
|
-
axisSectionWidth={leftAxisWidth}
|
|
1535
|
-
side="left"
|
|
1536
|
-
height={panel.height}
|
|
1537
|
-
cursorRow={cursorRow}
|
|
1538
|
-
cursorPixelY={cursorPixelY}
|
|
1539
|
-
cursorLabel={leftCursorLabel}
|
|
1540
|
-
cursorColor={colors.crosshair}
|
|
1541
|
-
cursorBackgroundColor={colors.background}
|
|
1542
|
-
axisColor={colors.textDim}
|
|
1543
|
-
extraMarkers={leftAxisMarkers}
|
|
1544
|
-
/>
|
|
1545
|
-
<Box width={axisGap} />
|
|
1546
|
-
</>
|
|
1547
|
-
) : null}
|
|
1548
|
-
<ChartSurface
|
|
1549
|
-
ref={plotRef}
|
|
1550
|
-
width={plotWidth}
|
|
1551
|
-
height={panel.height}
|
|
1552
|
-
flexDirection="column"
|
|
1553
|
-
bitmaps={bitmapLayers}
|
|
1554
|
-
crosshair={crosshair}
|
|
1555
|
-
vectors={vectors}
|
|
1556
|
-
onMouseMove={interactive ? handleMouseMove : undefined}
|
|
1557
|
-
onMouseDown={interactive ? navigable ? startDrag : pressCursor : undefined}
|
|
1558
|
-
onMouseDrag={interactive && navigable ? dragViewport : undefined}
|
|
1559
|
-
onMouseUp={interactive && navigable ? resetDrag : undefined}
|
|
1560
|
-
onMouseDragEnd={interactive && navigable ? resetDrag : undefined}
|
|
1561
|
-
onMouseScroll={interactive && navigable ? panFromWheel : undefined}
|
|
1562
|
-
onMouseOut={interactive ? clearCursor : undefined}
|
|
1563
|
-
cursor={interactive ? toolDrag || !navigable || onPickTime ? "crosshair" : "grab" : undefined}
|
|
1564
|
-
data-gloom-interactive={interactive ? "true" : undefined}
|
|
1565
|
-
data-gloom-role={COMPOSITE_PANEL_ROLE}
|
|
1566
|
-
data-gloom-remote-kind={remoteKind}
|
|
1567
|
-
data-gloom-label={panel.label ?? panel.id}
|
|
1568
|
-
>
|
|
1569
|
-
{textLines.map((line, index) => <Text key={index} fg={colors.text}>{line}</Text>)}
|
|
1570
|
-
</ChartSurface>
|
|
1571
|
-
{rightAxisWidth > 0 ? (
|
|
1572
|
-
<>
|
|
1573
|
-
<Box width={axisGap} />
|
|
1574
|
-
<PriceAxisLabels
|
|
1575
|
-
axisLabels={rightAxisLabels}
|
|
1576
|
-
axisTicks={rightAxisTicks}
|
|
1577
|
-
axisWidth={rightAxisWidth}
|
|
1578
|
-
axisSectionWidth={rightAxisWidth}
|
|
1579
|
-
side="right"
|
|
1580
|
-
height={panel.height}
|
|
1581
|
-
cursorRow={cursorRow}
|
|
1582
|
-
cursorPixelY={cursorPixelY}
|
|
1583
|
-
cursorLabel={rightCursorLabel}
|
|
1584
|
-
cursorColor={colors.crosshair}
|
|
1585
|
-
cursorBackgroundColor={colors.background}
|
|
1586
|
-
axisColor={colors.textDim}
|
|
1587
|
-
extraMarkers={rightAxisMarkers}
|
|
1588
|
-
/>
|
|
1589
|
-
</>
|
|
1590
|
-
) : null}
|
|
1591
|
-
</Box>
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
function legendValue(
|
|
1596
|
-
series: ResolvedSeries,
|
|
1597
|
-
value: number | null,
|
|
1598
|
-
formatValue: CompositeChartProps["formatValue"],
|
|
1599
|
-
scene: CompositeChartScene | null,
|
|
1600
|
-
): string {
|
|
1601
|
-
if (value === null) return "—";
|
|
1602
|
-
return formatValue ? formatValue(value, series) : formatCompositeSeriesValue(value, series, axisPriceReference(scene, series));
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
/** The reference the axis holding a series keeps for its asset, so an average
|
|
1606
|
-
* of the price shows the price's decimals rather than its own float tail. A
|
|
1607
|
-
* series far from that price (another coin on a shared axis) keeps its own. */
|
|
1608
|
-
function axisPriceReference(scene: CompositeChartScene | null, series: ResolvedSeries): number | undefined {
|
|
1609
|
-
const own = seriesPriceReference(series);
|
|
1610
|
-
for (const panel of scene?.panels ?? []) {
|
|
1611
|
-
for (const domain of [panel.axes.left, panel.axes.right]) {
|
|
1612
|
-
if (!domain?.seriesIds.includes(series.id)) continue;
|
|
1613
|
-
const shared = domain.priceReferences?.[series.priceAssetCategory ?? ""];
|
|
1614
|
-
if (shared === undefined || own === undefined) return own ?? shared;
|
|
1615
|
-
const ratio = Math.abs(own / shared);
|
|
1616
|
-
return ratio > 0.1 && ratio < 10 ? shared : own;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
return own;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
const LISTED_TICKER_PATTERN = /(^|[\s(])([A-Z0-9^][A-Z0-9.^=/-]*):([A-Z0-9]{2,})(?=$|[\s),])/g;
|
|
1623
|
-
|
|
1624
|
-
/** `Volume AAPL:XNAS Price` → `Volume AAPL Price`, for a legend name that must shorten. */
|
|
1625
|
-
function withoutListingExchange(label: string): string {
|
|
1626
|
-
return label.replace(LISTED_TICKER_PATTERN, (match, lead: string, symbol: string, exchange: string) => (
|
|
1627
|
-
CANONICAL_EXCHANGE_ALIASES[exchange] ? `${lead}${symbol}` : match
|
|
1628
|
-
));
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
function CompositeLegend({
|
|
1632
|
-
scene,
|
|
1633
|
-
series,
|
|
1634
|
-
visibleSeriesIds,
|
|
1635
|
-
width,
|
|
1636
|
-
accessory,
|
|
1637
|
-
accessoryWidth,
|
|
1638
|
-
formatValue,
|
|
1639
|
-
showLatestChangePercent,
|
|
1640
|
-
onActivate,
|
|
1641
|
-
onToggleSeries,
|
|
1642
|
-
isSeriesToggleable,
|
|
1643
|
-
keyboardIndex,
|
|
1644
|
-
}: {
|
|
1645
|
-
scene: CompositeChartScene | null;
|
|
1646
|
-
series: ResolvedSeries[];
|
|
1647
|
-
visibleSeriesIds: ReadonlySet<string>;
|
|
1648
|
-
width: number;
|
|
1649
|
-
accessory: CompositeChartProps["legendAccessory"];
|
|
1650
|
-
accessoryWidth: CompositeChartProps["legendAccessoryWidth"];
|
|
1651
|
-
formatValue: CompositeChartProps["formatValue"];
|
|
1652
|
-
showLatestChangePercent: CompositeChartProps["showLatestChangePercent"];
|
|
1653
|
-
onActivate: CompositeChartProps["onActivate"];
|
|
1654
|
-
onToggleSeries: CompositeChartProps["onToggleSeries"];
|
|
1655
|
-
isSeriesToggleable: CompositeChartProps["isSeriesToggleable"];
|
|
1656
|
-
keyboardIndex?: number | null;
|
|
1657
|
-
}) {
|
|
1658
|
-
const isDesktopWeb = useUiCapabilities().nativePaneChrome === true;
|
|
1659
|
-
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
1660
|
-
const cursorValueById = new Map(
|
|
1661
|
-
scene?.cursorValues.map((entry) => [entry.seriesId, entry] as const) ?? [],
|
|
1662
|
-
);
|
|
1663
|
-
const entries = series.map((entry) => {
|
|
1664
|
-
const toggleable = !!onToggleSeries && (isSeriesToggleable?.(entry) ?? true);
|
|
1665
|
-
const cursorValue = cursorValueById.get(entry.id);
|
|
1666
|
-
const changeText = showLatestChangePercent
|
|
1667
|
-
&& !scene?.cursorDate
|
|
1668
|
-
&& typeof entry.latestChangePercent === "number"
|
|
1669
|
-
&& Number.isFinite(entry.latestChangePercent)
|
|
1670
|
-
? ` ${formatPercentRaw(entry.latestChangePercent)}`
|
|
1671
|
-
: "";
|
|
1672
|
-
const valueText = entry.points.length === 0
|
|
1673
|
-
? entry.hidden ? "" : "no data"
|
|
1674
|
-
: `${legendValue(
|
|
1675
|
-
entry,
|
|
1676
|
-
cursorValue?.value ?? null,
|
|
1677
|
-
formatValue,
|
|
1678
|
-
scene,
|
|
1679
|
-
)}${changeText}`;
|
|
1680
|
-
const fullText = [entry.label, valueText].filter(Boolean).join(" ");
|
|
1681
|
-
const details = formatCompositePointDetails(cursorValue?.point);
|
|
1682
|
-
const exactTotal = entry.unitGroup.split(":")[0] === "currency-total"
|
|
1683
|
-
&& cursorValue?.value != null && Number.isFinite(cursorValue.value)
|
|
1684
|
-
? `Value ${cursorValue.value} ${entry.unit}` : "";
|
|
1685
|
-
const tooltip = [fullText, exactTotal, details].filter(Boolean).join(" · ");
|
|
1686
|
-
const label = entry.label;
|
|
1687
|
-
const compactLabel = withoutListingExchange(label);
|
|
1688
|
-
const labelWidth = displayWidth(label);
|
|
1689
|
-
const text = [label, valueText].filter(Boolean).join(" ");
|
|
1690
|
-
return {
|
|
1691
|
-
entry,
|
|
1692
|
-
label,
|
|
1693
|
-
compactLabel,
|
|
1694
|
-
text,
|
|
1695
|
-
width: Math.max(1, displayWidth(text)) + 2,
|
|
1696
|
-
labelWidth,
|
|
1697
|
-
valueText,
|
|
1698
|
-
toggleable,
|
|
1699
|
-
tooltip,
|
|
1700
|
-
};
|
|
1701
|
-
});
|
|
1702
|
-
type LegendEntry = (typeof entries)[number];
|
|
1703
|
-
const setEntryLabel = (target: LegendEntry, label: string, labelWidth: number) => {
|
|
1704
|
-
target.label = label;
|
|
1705
|
-
target.labelWidth = labelWidth;
|
|
1706
|
-
target.text = [truncateToDisplayWidth(label, labelWidth), target.valueText].filter(Boolean).join(" ");
|
|
1707
|
-
target.width = Math.max(1, displayWidth(target.text)) + 2;
|
|
1708
|
-
};
|
|
1709
|
-
const measureSeriesWidth = () => entries.reduce(
|
|
1710
|
-
(total, entry, index) => total + entry.width + (index > 0 ? 1 : 0),
|
|
1711
|
-
0,
|
|
1712
|
-
);
|
|
1713
|
-
// One cell in from the pane border, so the first marker and the legend text
|
|
1714
|
-
// line up with the query bar and tables above and below the chart.
|
|
1715
|
-
const inset = width > 1 ? 1 : 0;
|
|
1716
|
-
const innerWidth = Math.max(0, width - inset);
|
|
1717
|
-
const resolvedAccessoryWidth = accessory
|
|
1718
|
-
? Math.max(1, Math.min(innerWidth, Math.floor(accessoryWidth ?? 14)))
|
|
1719
|
-
: 0;
|
|
1720
|
-
const reservedAccessoryGap = accessory && innerWidth > resolvedAccessoryWidth ? 1 : 0;
|
|
1721
|
-
// The cursor date lives on the time axis, where the crosshair points at it.
|
|
1722
|
-
const widthBeforeAccessory = Math.max(0, innerWidth - resolvedAccessoryWidth - reservedAccessoryGap);
|
|
1723
|
-
// Names get the room the row has. When it overflows, the widest name
|
|
1724
|
-
// shortens first: its listing exchange goes before any of the name is cut.
|
|
1725
|
-
// The value (including its sign/unit) always stays intact. If the row cannot
|
|
1726
|
-
// fit even with every name at a readable fragment, it keeps the old 30-cell
|
|
1727
|
-
// entries and scrolls.
|
|
1728
|
-
const minimumSeriesWidth = entries.reduce((total, entry, index) => {
|
|
1729
|
-
const text = [truncateToDisplayWidth(entry.compactLabel, Math.min(8, displayWidth(entry.compactLabel))), entry.valueText]
|
|
1730
|
-
.filter(Boolean).join(" ");
|
|
1731
|
-
return total + Math.max(1, displayWidth(text)) + 2 + (index > 0 ? 1 : 0);
|
|
1732
|
-
}, 0);
|
|
1733
|
-
if (minimumSeriesWidth <= widthBeforeAccessory) {
|
|
1734
|
-
while (measureSeriesWidth() > widthBeforeAccessory) {
|
|
1735
|
-
const shrinkable = entries
|
|
1736
|
-
.filter((entry) => entry.label !== entry.compactLabel || entry.labelWidth > 8)
|
|
1737
|
-
.sort((left, right) => right.labelWidth - left.labelWidth)[0];
|
|
1738
|
-
if (!shrinkable) break;
|
|
1739
|
-
if (shrinkable.label !== shrinkable.compactLabel) {
|
|
1740
|
-
setEntryLabel(shrinkable, shrinkable.compactLabel, Math.min(shrinkable.labelWidth, displayWidth(shrinkable.compactLabel)));
|
|
1741
|
-
} else {
|
|
1742
|
-
setEntryLabel(shrinkable, shrinkable.label, shrinkable.labelWidth - 1);
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
} else {
|
|
1746
|
-
for (const entry of entries) {
|
|
1747
|
-
const budget = Math.max(0, 30 - (entry.valueText ? displayWidth(entry.valueText) + 1 : 0));
|
|
1748
|
-
if (entry.labelWidth > budget) setEntryLabel(entry, entry.compactLabel, budget);
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
const desiredSeriesWidth = measureSeriesWidth();
|
|
1752
|
-
const seriesWidth = Math.min(desiredSeriesWidth, widthBeforeAccessory);
|
|
1753
|
-
const accessorySpacerWidth = accessory
|
|
1754
|
-
? Math.max(reservedAccessoryGap, innerWidth - seriesWidth - resolvedAccessoryWidth)
|
|
1755
|
-
: 0;
|
|
1756
|
-
const keyboardEntryStart = keyboardIndex === null || keyboardIndex === undefined
|
|
1757
|
-
? null
|
|
1758
|
-
: entries.slice(0, keyboardIndex).reduce(
|
|
1759
|
-
(total, entry, index) => total + entry.width + (index > 0 ? 1 : 0),
|
|
1760
|
-
0,
|
|
1761
|
-
) + (keyboardIndex > 0 ? 1 : 0);
|
|
1762
|
-
const keyboardEntryEnd = keyboardEntryStart === null
|
|
1763
|
-
? null
|
|
1764
|
-
: keyboardEntryStart + (entries[keyboardIndex!]?.width ?? 0);
|
|
1765
|
-
|
|
1766
|
-
useEffect(() => {
|
|
1767
|
-
if (keyboardEntryStart === null || keyboardEntryEnd === null) return;
|
|
1768
|
-
const scrollBox = scrollRef.current;
|
|
1769
|
-
const viewportWidth = scrollBox?.viewport?.width || scrollBox?.width || seriesWidth;
|
|
1770
|
-
if (!scrollBox || viewportWidth <= 0) return;
|
|
1771
|
-
const currentLeft = scrollBox.scrollLeft ?? 0;
|
|
1772
|
-
const nextLeft = keyboardEntryStart < currentLeft
|
|
1773
|
-
? keyboardEntryStart
|
|
1774
|
-
: keyboardEntryEnd > currentLeft + viewportWidth
|
|
1775
|
-
? keyboardEntryEnd - viewportWidth
|
|
1776
|
-
: currentLeft;
|
|
1777
|
-
if (nextLeft === currentLeft) return;
|
|
1778
|
-
scrollBox.scrollLeft = nextLeft;
|
|
1779
|
-
scrollBox.scrollTo({ x: nextLeft, y: scrollBox.scrollTop });
|
|
1780
|
-
}, [keyboardEntryEnd, keyboardEntryStart, seriesWidth]);
|
|
1781
|
-
const handleMouseScroll = (event?: {
|
|
1782
|
-
preventDefault?: () => void;
|
|
1783
|
-
stopPropagation?: () => void;
|
|
1784
|
-
scroll?: { direction?: string; delta?: number };
|
|
1785
|
-
}) => {
|
|
1786
|
-
const direction = event?.scroll?.direction;
|
|
1787
|
-
const scrollBox = scrollRef.current;
|
|
1788
|
-
const viewportWidth = scrollBox?.viewport?.width || scrollBox?.width || 0;
|
|
1789
|
-
const contentWidth = Math.max(desiredSeriesWidth, scrollBox?.scrollWidth ?? 0);
|
|
1790
|
-
if (!direction || !scrollBox || viewportWidth <= 0 || contentWidth <= viewportWidth) return;
|
|
1791
|
-
|
|
1792
|
-
event.preventDefault?.();
|
|
1793
|
-
event.stopPropagation?.();
|
|
1794
|
-
const rawDelta = Math.abs(event.scroll?.delta ?? 1);
|
|
1795
|
-
const deltaCells = Math.max(1, Math.round(rawDelta / LEGEND_WHEEL_DELTA_PER_CELL));
|
|
1796
|
-
const directionSign = direction === "right" || direction === "down" ? 1 : -1;
|
|
1797
|
-
const nextLeft = Math.max(
|
|
1798
|
-
0,
|
|
1799
|
-
Math.min(
|
|
1800
|
-
contentWidth - viewportWidth,
|
|
1801
|
-
(scrollBox.scrollLeft ?? 0) + directionSign * deltaCells,
|
|
1802
|
-
),
|
|
1803
|
-
);
|
|
1804
|
-
scrollBox.scrollLeft = nextLeft;
|
|
1805
|
-
scrollBox.scrollTo({ x: nextLeft, y: scrollBox.scrollTop });
|
|
1806
|
-
};
|
|
1807
|
-
return (
|
|
1808
|
-
<Box
|
|
1809
|
-
flexDirection="row"
|
|
1810
|
-
alignItems="flex-end"
|
|
1811
|
-
width={width}
|
|
1812
|
-
height={1}
|
|
1813
|
-
paddingLeft={inset}
|
|
1814
|
-
overflow="visible"
|
|
1815
|
-
// Accessory dropdowns must escape above the sibling drawing toolbar.
|
|
1816
|
-
zIndex={accessory ? 40 : 20}
|
|
1817
|
-
data-gloom-role="composite-chart-legend"
|
|
1818
|
-
>
|
|
1819
|
-
{seriesWidth > 0 ? (
|
|
1820
|
-
<ScrollBox
|
|
1821
|
-
ref={scrollRef}
|
|
1822
|
-
width={seriesWidth}
|
|
1823
|
-
height={1}
|
|
1824
|
-
flexShrink={0}
|
|
1825
|
-
scrollX
|
|
1826
|
-
focusable={false}
|
|
1827
|
-
horizontalScrollbarOptions={{ visible: false }}
|
|
1828
|
-
onMouseScroll={handleMouseScroll}
|
|
1829
|
-
data-gloom-role="composite-chart-legend-scroll"
|
|
1830
|
-
>
|
|
1831
|
-
<Box flexDirection="row" width={desiredSeriesWidth} height={1} gap={1}>
|
|
1832
|
-
{entries.map(({ entry, text, toggleable, tooltip, width: entryWidth }, index) => {
|
|
1833
|
-
const entryVisible = visibleSeriesIds.has(entry.id);
|
|
1834
|
-
return (
|
|
1835
|
-
<Box
|
|
1836
|
-
key={entry.id}
|
|
1837
|
-
flexDirection="row"
|
|
1838
|
-
alignItems="center"
|
|
1839
|
-
width={entryWidth}
|
|
1840
|
-
height={1}
|
|
1841
|
-
flexShrink={0}
|
|
1842
|
-
overflow="hidden"
|
|
1843
|
-
backgroundColor={keyboardIndex === index ? themeColors.selected : undefined}
|
|
1844
|
-
hoverBackgroundColor={toggleable ? hoverBg() : undefined}
|
|
1845
|
-
onMouseDown={toggleable ? (event: ChartMouseEvent) => {
|
|
1846
|
-
onActivate?.();
|
|
1847
|
-
consumeChartMouseEvent(event);
|
|
1848
|
-
onToggleSeries?.(entry.id);
|
|
1849
|
-
} : undefined}
|
|
1850
|
-
cursor={toggleable ? "pointer" : undefined}
|
|
1851
|
-
data-gloom-interactive={toggleable ? "true" : undefined}
|
|
1852
|
-
data-gloom-role="composite-chart-legend-series"
|
|
1853
|
-
data-gloom-label={`${toggleable
|
|
1854
|
-
? `${entryVisible ? "Hide" : "Show"} `
|
|
1855
|
-
: ""}${tooltip}`}
|
|
1856
|
-
data-visible={entryVisible ? "true" : "false"}
|
|
1857
|
-
title={isDesktopWeb ? tooltip : undefined}
|
|
1858
|
-
>
|
|
1859
|
-
{isDesktopWeb ? (
|
|
1860
|
-
<Box
|
|
1861
|
-
flexShrink={0}
|
|
1862
|
-
style={{
|
|
1863
|
-
width: 8,
|
|
1864
|
-
height: 8,
|
|
1865
|
-
marginInlineEnd: 6,
|
|
1866
|
-
borderRadius: 999,
|
|
1867
|
-
border: `1px solid ${entry.color}`,
|
|
1868
|
-
backgroundColor: entryVisible ? entry.color : "transparent",
|
|
1869
|
-
}}
|
|
1870
|
-
data-gloom-role="composite-chart-legend-marker"
|
|
1871
|
-
/>
|
|
1872
|
-
) : (
|
|
1873
|
-
<Text fg={entryVisible ? entry.color : themeColors.textMuted}>● </Text>
|
|
1874
|
-
)}
|
|
1875
|
-
{/* The filled/hollow marker already says whether a series is
|
|
1876
|
-
shown; the word only repeated it in every legend slot. */}
|
|
1877
|
-
<Text fg={entryVisible ? themeColors.text : themeColors.textDim}>{text}</Text>
|
|
1878
|
-
</Box>
|
|
1879
|
-
);
|
|
1880
|
-
})}
|
|
1881
|
-
</Box>
|
|
1882
|
-
</ScrollBox>
|
|
1883
|
-
) : null}
|
|
1884
|
-
{accessorySpacerWidth > 0 ? (
|
|
1885
|
-
<Box width={accessorySpacerWidth} flexShrink={0} />
|
|
1886
|
-
) : null}
|
|
1887
|
-
{accessory ? (
|
|
1888
|
-
<Box
|
|
1889
|
-
position="relative"
|
|
1890
|
-
width={resolvedAccessoryWidth}
|
|
1891
|
-
flexShrink={0}
|
|
1892
|
-
height={1}
|
|
1893
|
-
overflow="visible"
|
|
1894
|
-
zIndex={21}
|
|
1895
|
-
>
|
|
1896
|
-
{accessory}
|
|
1897
|
-
</Box>
|
|
1898
|
-
) : null}
|
|
1899
|
-
</Box>
|
|
1900
|
-
);
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
const NO_DRAWINGS: readonly ChartDrawing[] = [];
|
|
1904
58
|
const NO_X_MARKERS: readonly CompositeChartXMarker[] = [];
|
|
1905
|
-
/** Coalesces a drag into one write instead of one per pointer move. */
|
|
1906
|
-
const DRAWING_PERSIST_DELAY_MS = 400;
|
|
1907
|
-
|
|
1908
|
-
/**
|
|
1909
|
-
* Drawings are anchored to data, so they outlive the mounted chart: they ride
|
|
1910
|
-
* along with the pane settings that already carry the chart spec. Only mounted
|
|
1911
|
-
* inside a pane, so a standalone chart still renders without app state.
|
|
1912
|
-
*/
|
|
1913
|
-
function ChartDrawingStore({
|
|
1914
|
-
paneInstanceId,
|
|
1915
|
-
drawings,
|
|
1916
|
-
onRestore,
|
|
1917
|
-
}: {
|
|
1918
|
-
paneInstanceId: string;
|
|
1919
|
-
drawings: readonly ChartDrawing[];
|
|
1920
|
-
onRestore: (drawings: readonly ChartDrawing[]) => void;
|
|
1921
|
-
}) {
|
|
1922
|
-
const [stored, setStored] = usePaneSettingValue<readonly ChartDrawing[]>(
|
|
1923
|
-
CHART_DRAWINGS_SETTING_KEY,
|
|
1924
|
-
NO_DRAWINGS,
|
|
1925
|
-
paneInstanceId,
|
|
1926
|
-
);
|
|
1927
|
-
const restoredRef = useRef<readonly ChartDrawing[] | null>(null);
|
|
1928
|
-
if (restoredRef.current === null) {
|
|
1929
|
-
restoredRef.current = parseChartDrawings(stored);
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
useEffect(() => {
|
|
1933
|
-
const restored = restoredRef.current;
|
|
1934
|
-
if (restored && restored.length > 0) onRestore(restored);
|
|
1935
|
-
// Restoring once on mount: later writes must not scroll back in time.
|
|
1936
|
-
}, []);
|
|
1937
|
-
|
|
1938
|
-
useEffect(() => {
|
|
1939
|
-
const restored = restoredRef.current ?? NO_DRAWINGS;
|
|
1940
|
-
if (drawings === restored) return;
|
|
1941
|
-
if (drawings.length === 0 && restored.length === 0) return;
|
|
1942
|
-
const timer = setTimeout(() => setStored(drawings), DRAWING_PERSIST_DELAY_MS);
|
|
1943
|
-
return () => clearTimeout(timer);
|
|
1944
|
-
}, [drawings, setStored]);
|
|
1945
|
-
|
|
1946
|
-
return null;
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
59
|
export function CompositeChart({
|
|
1950
60
|
series,
|
|
1951
61
|
legendSeries,
|
|
@@ -1986,16 +96,11 @@ export function CompositeChart({
|
|
|
1986
96
|
const isDesktopWeb = useUiCapabilities().nativePaneChrome === true;
|
|
1987
97
|
const showTextFallback = useShowChartTextFallback();
|
|
1988
98
|
const [internalCursorDate, setInternalCursorDate] = useState<Date | null>(null);
|
|
1989
|
-
const [legendKeyboardIndex, setLegendKeyboardIndex] = useState<number | null>(null);
|
|
1990
99
|
const [toolSpan, setToolSpan] = useState<ChartToolSpan | null>(null);
|
|
1991
|
-
const [armedTool, setArmedTool] = useState<ChartToolKind | null>(null);
|
|
1992
|
-
const [keyboardPlacement, setKeyboardPlacement] = useState<KeyboardToolPlacement | null>(null);
|
|
1993
|
-
const keyboardId = `composite-chart:${useId()}`;
|
|
1994
100
|
const paneInstanceId = useOptionalPaneInstanceId();
|
|
1995
101
|
const [drawings, setDrawings] = useState<readonly ChartDrawing[]>(NO_DRAWINGS);
|
|
1996
102
|
const [selectedDrawingId, setSelectedDrawingId] = useState<string | null>(null);
|
|
1997
103
|
const [drawColor, setDrawColor] = useState<string>(CHART_DRAWING_COLORS[0]);
|
|
1998
|
-
const [selectedLevelId, setSelectedLevelId] = useState<string | null>(null);
|
|
1999
104
|
const addDrawing = useCallback((drawing: ChartDrawing) => {
|
|
2000
105
|
setDrawings((current) => [...current, drawing]);
|
|
2001
106
|
setSelectedDrawingId(drawing.id);
|
|
@@ -2050,126 +155,13 @@ export function CompositeChart({
|
|
|
2050
155
|
() => new Set(visibleSeries.map((entry) => entry.id)),
|
|
2051
156
|
[visibleSeries],
|
|
2052
157
|
);
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
}
|
|
2060
|
-
const previousAuthoredViewportRef = useRef<CompositeViewportRange | null>(viewport ?? null);
|
|
2061
|
-
const previousViewportResetKeyRef = useRef(viewportResetKey);
|
|
2062
|
-
// The user owns this once they navigate. Data refreshes never rewrite it;
|
|
2063
|
-
// only the authored viewport changing or an explicit reset clears it.
|
|
2064
|
-
const [userViewport, setUserViewport] = useState<CompositeViewportRange | null>(null);
|
|
2065
|
-
const hasViewportResetKey = viewportResetKey !== undefined
|
|
2066
|
-
|| previousViewportResetKeyRef.current !== undefined;
|
|
2067
|
-
const authoredViewportChanged = hasViewportResetKey
|
|
2068
|
-
? previousViewportResetKeyRef.current !== viewportResetKey
|
|
2069
|
-
: shouldResetCompositeViewport(
|
|
2070
|
-
previousAuthoredViewportRef.current,
|
|
2071
|
-
viewport ?? null,
|
|
2072
|
-
);
|
|
2073
|
-
const navigationFrame = useMemo(
|
|
2074
|
-
() => buildCompositeNavigationFrame(visibleSeries, marketTimelineSeries, {
|
|
2075
|
-
historicalPaddingRatio: allowHistoricalBackfill ? HISTORICAL_PADDING_RATIO : 0,
|
|
2076
|
-
}),
|
|
2077
|
-
[allowHistoricalBackfill, marketTimelineSeries, visibleSeries],
|
|
2078
|
-
);
|
|
2079
|
-
const authoredViewport = useMemo(() => {
|
|
2080
|
-
// The requested dates own the axis, including gaps in a cached or partial
|
|
2081
|
-
// response. Fitting them to available observations silently changes the
|
|
2082
|
-
// research period. User pan/zoom gestures retain their navigation limits.
|
|
2083
|
-
if (viewport && Number.isFinite(viewport.start.getTime())
|
|
2084
|
-
&& Number.isFinite(viewport.end.getTime()) && viewport.start <= viewport.end) return viewport;
|
|
2085
|
-
return navigationFrame ? compositeNavigationDataViewport(navigationFrame) : null;
|
|
2086
|
-
}, [navigationFrame, viewport]);
|
|
2087
|
-
const activeUserViewport = authoredViewportChanged ? null : userViewport;
|
|
2088
|
-
const effectiveViewport = activeUserViewport ?? authoredViewport;
|
|
2089
|
-
const userViewportStart = activeUserViewport?.start.getTime() ?? null;
|
|
2090
|
-
const userViewportEnd = activeUserViewport?.end.getTime() ?? null;
|
|
2091
|
-
const lastReportedViewportRef = useRef<string | null>(null);
|
|
2092
|
-
const viewportInteractionRef = useRef<"pan" | "reset" | "zoom">("reset");
|
|
2093
|
-
useEffect(() => {
|
|
2094
|
-
if (!onViewportChange) return;
|
|
2095
|
-
const key = userViewportStart === null || userViewportEnd === null
|
|
2096
|
-
? "none"
|
|
2097
|
-
: `${userViewportStart}:${userViewportEnd}`;
|
|
2098
|
-
// The callback drives adaptive data loading. Seed it from the authored
|
|
2099
|
-
// viewport without echoing that controlled value back into the loader.
|
|
2100
|
-
if (lastReportedViewportRef.current === null) {
|
|
2101
|
-
lastReportedViewportRef.current = key;
|
|
2102
|
-
return;
|
|
2103
|
-
}
|
|
2104
|
-
if (lastReportedViewportRef.current === key) return;
|
|
2105
|
-
lastReportedViewportRef.current = key;
|
|
2106
|
-
onViewportChange(
|
|
2107
|
-
userViewportStart === null || userViewportEnd === null
|
|
2108
|
-
? null
|
|
2109
|
-
: { start: new Date(userViewportStart), end: new Date(userViewportEnd) },
|
|
2110
|
-
viewportInteractionRef.current,
|
|
2111
|
-
);
|
|
2112
|
-
}, [onViewportChange, userViewportEnd, userViewportStart]);
|
|
2113
|
-
|
|
2114
|
-
useEffect(() => {
|
|
2115
|
-
previousAuthoredViewportRef.current = viewport ?? null;
|
|
2116
|
-
previousViewportResetKeyRef.current = viewportResetKey;
|
|
2117
|
-
if (authoredViewportChanged && userViewport) {
|
|
2118
|
-
viewportInteractionRef.current = "reset";
|
|
2119
|
-
setUserViewport(null);
|
|
2120
|
-
}
|
|
2121
|
-
}, [authoredViewportChanged, userViewport, viewport, viewportResetKey]);
|
|
2122
|
-
|
|
2123
|
-
const navigate = useCallback((
|
|
2124
|
-
kind: "pan" | "zoom",
|
|
2125
|
-
compute: (base: CompositeViewportRange, frame: CompositeNavigationFrame) => CompositeViewportRange,
|
|
2126
|
-
gesture?: { frame: CompositeNavigationFrame; from: CompositeViewportRange },
|
|
2127
|
-
) => {
|
|
2128
|
-
const frame = gesture?.frame ?? navigationFrame;
|
|
2129
|
-
if (!frame) return;
|
|
2130
|
-
viewportInteractionRef.current = kind;
|
|
2131
|
-
setUserViewport((current) => {
|
|
2132
|
-
const base = gesture?.from ?? current ?? authoredViewport;
|
|
2133
|
-
if (!base) return current;
|
|
2134
|
-
const next = compute(base, frame);
|
|
2135
|
-
// Once navigated, the window is the user's until they reset it, even if
|
|
2136
|
-
// a gesture happens to land back on the authored range: the owner may
|
|
2137
|
-
// echo a navigated range back as the authored one, and dropping to null
|
|
2138
|
-
// there would reload the original range under the pointer.
|
|
2139
|
-
return sameCompositeViewport(next, current ?? base) ? current : next;
|
|
2140
|
-
});
|
|
2141
|
-
}, [authoredViewport, navigationFrame]);
|
|
2142
|
-
const panViewport = useCallback((
|
|
2143
|
-
deltaPositions: number,
|
|
2144
|
-
gesture?: { frame: CompositeNavigationFrame; from: CompositeViewportRange },
|
|
2145
|
-
) => {
|
|
2146
|
-
navigate("pan", (base, frame) => panCompositeViewport(frame, base, deltaPositions), gesture);
|
|
2147
|
-
}, [navigate]);
|
|
2148
|
-
const panViewportByRatio = useCallback((shiftRatio: number) => {
|
|
2149
|
-
if (!navigationFrame || !effectiveViewport) return;
|
|
2150
|
-
const positions = compositeViewportPositions(navigationFrame, effectiveViewport);
|
|
2151
|
-
if (!positions) return;
|
|
2152
|
-
panViewport(shiftRatio * Math.max(positions.end - positions.start, Number.EPSILON));
|
|
2153
|
-
}, [effectiveViewport, navigationFrame, panViewport]);
|
|
2154
|
-
const zoomViewport = useCallback((zoomFactor: number, anchorRatio = 1) => {
|
|
2155
|
-
navigate("zoom", (base, frame) => zoomCompositeViewport(frame, base, zoomFactor, anchorRatio));
|
|
2156
|
-
}, [navigate]);
|
|
2157
|
-
const setViewportRange = useCallback((range: CompositeViewportRange) => {
|
|
2158
|
-
navigate("zoom", (_base, frame) => fitCompositeViewport(frame, range));
|
|
2159
|
-
}, [navigate]);
|
|
2160
|
-
const resetViewport = useCallback(() => {
|
|
2161
|
-
viewportInteractionRef.current = "reset";
|
|
2162
|
-
setUserViewport(null);
|
|
2163
|
-
}, []);
|
|
2164
|
-
// Sticky while armed: the toolbar chip shows which tool owns the drag, and a
|
|
2165
|
-
// one-shot tool would blink off before the user could see it.
|
|
2166
|
-
const armTool = useCallback((tool: ChartToolKind | null) => {
|
|
2167
|
-
setArmedTool((current) => current === tool ? null : tool);
|
|
2168
|
-
setKeyboardPlacement(null);
|
|
2169
|
-
if (tool === null) setSelectedDrawingId(null);
|
|
2170
|
-
if (tool !== "level") setSelectedLevelId(null);
|
|
2171
|
-
}, []);
|
|
2172
|
-
const cancelKeyboardPlacement = useCallback(() => setKeyboardPlacement(null), []);
|
|
158
|
+
const {
|
|
159
|
+
navigationFrame, activeUserViewport, effectiveViewport,
|
|
160
|
+
panViewport, panViewportByRatio, zoomViewport, setViewportRange, resetViewport,
|
|
161
|
+
} = useCompositeViewport({
|
|
162
|
+
viewport, viewportResetKey, visibleSeries, marketTimelineSeries,
|
|
163
|
+
allowHistoricalBackfill, onViewportChange,
|
|
164
|
+
});
|
|
2173
165
|
const legendRows = showLegend && (visibleSeries.length > 0 || legendAccessory)
|
|
2174
166
|
? 1
|
|
2175
167
|
: 0;
|
|
@@ -2296,10 +288,66 @@ export function CompositeChart({
|
|
|
2296
288
|
)
|
|
2297
289
|
: null
|
|
2298
290
|
), [baseScene, normalizedCursorTimestamp]);
|
|
291
|
+
const lastCursorTimestampRef = useRef<number | null>(normalizedCursorTimestamp);
|
|
292
|
+
const renderedCursorTimestampRef = useRef<number | null>(normalizedCursorTimestamp);
|
|
293
|
+
if (renderedCursorTimestampRef.current !== normalizedCursorTimestamp) {
|
|
294
|
+
renderedCursorTimestampRef.current = normalizedCursorTimestamp;
|
|
295
|
+
lastCursorTimestampRef.current = normalizedCursorTimestamp;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const updateCursor = useCallback((date: Date | null) => {
|
|
299
|
+
const timestamp = date?.getTime() ?? null;
|
|
300
|
+
const nextTimestamp = timestamp !== null && Number.isFinite(timestamp) ? timestamp : null;
|
|
301
|
+
if (lastCursorTimestampRef.current === nextTimestamp) return;
|
|
302
|
+
lastCursorTimestampRef.current = nextTimestamp;
|
|
303
|
+
if (cursorDate === undefined) setInternalCursorDate(date);
|
|
304
|
+
onCursorDateChange?.(date);
|
|
305
|
+
}, [cursorDate, onCursorDateChange]);
|
|
306
|
+
|
|
307
|
+
const {
|
|
308
|
+
armedTool,
|
|
309
|
+
armTool,
|
|
310
|
+
cancelKeyboardPlacement,
|
|
311
|
+
activateFromPointer,
|
|
312
|
+
legendKeyboardIndex,
|
|
313
|
+
levelHost,
|
|
314
|
+
levelsEditable,
|
|
315
|
+
selectedLevelId,
|
|
316
|
+
setSelectedLevelId,
|
|
317
|
+
keyboardToolDrag,
|
|
318
|
+
} = useCompositeChartKeyboard({
|
|
319
|
+
focused,
|
|
320
|
+
interactive,
|
|
321
|
+
navigable,
|
|
322
|
+
showLegend,
|
|
323
|
+
scene,
|
|
324
|
+
cursorDate,
|
|
325
|
+
updateCursor,
|
|
326
|
+
onActivate,
|
|
327
|
+
timePick,
|
|
328
|
+
levels,
|
|
329
|
+
visibleLegendSeries,
|
|
330
|
+
visibleSeriesIds,
|
|
331
|
+
onToggleSeries,
|
|
332
|
+
isSeriesToggleable,
|
|
333
|
+
navigationFrame,
|
|
334
|
+
activeUserViewport,
|
|
335
|
+
panViewportByRatio,
|
|
336
|
+
zoomViewport,
|
|
337
|
+
setViewportRange,
|
|
338
|
+
resetViewport,
|
|
339
|
+
drawings,
|
|
340
|
+
selectedDrawingId,
|
|
341
|
+
setSelectedDrawingId,
|
|
342
|
+
drawColor,
|
|
343
|
+
addDrawing,
|
|
344
|
+
removeDrawing,
|
|
345
|
+
pickDrawColor,
|
|
346
|
+
});
|
|
2299
347
|
const handleEmptyMouseScroll = useCallback((event: ChartMouseEvent) => {
|
|
2300
348
|
const scroll = event.scroll;
|
|
2301
349
|
if (!interactive || !navigationFrame || !scroll) return;
|
|
2302
|
-
|
|
350
|
+
activateFromPointer();
|
|
2303
351
|
consumeChartMouseEvent(event);
|
|
2304
352
|
if (event.modifiers.ctrl && isVerticalWheelDirection(scroll.direction)) {
|
|
2305
353
|
const factor = resolveCompositeWheelZoom(scroll);
|
|
@@ -2312,511 +360,15 @@ export function CompositeChart({
|
|
|
2312
360
|
}
|
|
2313
361
|
panViewportByRatio(resolveCompositeWheelPan(scroll, plotWidth * cellWidthPx));
|
|
2314
362
|
}, [
|
|
363
|
+
activateFromPointer,
|
|
2315
364
|
cellWidthPx,
|
|
2316
365
|
interactive,
|
|
2317
366
|
navigationFrame,
|
|
2318
|
-
onActivate,
|
|
2319
367
|
panViewportByRatio,
|
|
2320
368
|
plotWidth,
|
|
2321
369
|
resetViewport,
|
|
2322
370
|
zoomViewport,
|
|
2323
371
|
]);
|
|
2324
|
-
const levelHost = useMemo(
|
|
2325
|
-
() => scene && levels ? levelPanel(scene, levels.seriesId) : null,
|
|
2326
|
-
[levels, scene],
|
|
2327
|
-
);
|
|
2328
|
-
const levelItems = levels?.items ?? [];
|
|
2329
|
-
const levelsEditable = !!levels?.onEdit && !!levelHost;
|
|
2330
|
-
const selectedLevel = levelItems.find((level) => level.id === selectedLevelId) ?? null;
|
|
2331
|
-
useEffect(() => {
|
|
2332
|
-
if (selectedLevelId && !selectedLevel) setSelectedLevelId(null);
|
|
2333
|
-
}, [selectedLevel, selectedLevelId]);
|
|
2334
|
-
const keyboardCursorDateRef = useRef<Date | null>(scene?.cursorDate ?? null);
|
|
2335
|
-
keyboardCursorDateRef.current = scene?.cursorDate ?? null;
|
|
2336
|
-
const lastCursorTimestampRef = useRef<number | null>(normalizedCursorTimestamp);
|
|
2337
|
-
const renderedCursorTimestampRef = useRef<number | null>(normalizedCursorTimestamp);
|
|
2338
|
-
if (renderedCursorTimestampRef.current !== normalizedCursorTimestamp) {
|
|
2339
|
-
renderedCursorTimestampRef.current = normalizedCursorTimestamp;
|
|
2340
|
-
lastCursorTimestampRef.current = normalizedCursorTimestamp;
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
const updateCursor = useCallback((date: Date | null) => {
|
|
2344
|
-
const timestamp = date?.getTime() ?? null;
|
|
2345
|
-
const nextTimestamp = timestamp !== null && Number.isFinite(timestamp) ? timestamp : null;
|
|
2346
|
-
if (lastCursorTimestampRef.current === nextTimestamp) return;
|
|
2347
|
-
lastCursorTimestampRef.current = nextTimestamp;
|
|
2348
|
-
if (cursorDate === undefined) setInternalCursorDate(date);
|
|
2349
|
-
onCursorDateChange?.(date);
|
|
2350
|
-
}, [cursorDate, onCursorDateChange]);
|
|
2351
|
-
|
|
2352
|
-
// The cursor keys belong to every focused chart; pan, zoom, the tools and
|
|
2353
|
-
// their keys only to one that navigates.
|
|
2354
|
-
const arrowsClaimed = usePaneArrowsClaimed();
|
|
2355
|
-
const keyboardActive = focused && interactive;
|
|
2356
|
-
const toolsActive = keyboardActive && navigable;
|
|
2357
|
-
useEffect(() => {
|
|
2358
|
-
if (!toolsActive) setKeyboardPlacement(null);
|
|
2359
|
-
}, [toolsActive]);
|
|
2360
|
-
const legendKeysActive = keyboardActive && showLegend && visibleLegendSeries.length > 0;
|
|
2361
|
-
const legendEntry = legendKeyboardIndex === null ? undefined : visibleLegendSeries[legendKeyboardIndex];
|
|
2362
|
-
const legendEntryToggleable = !!legendEntry && !!onToggleSeries && (isSeriesToggleable?.(legendEntry) ?? true);
|
|
2363
|
-
const anyLegendToggleable = legendKeysActive && !!onToggleSeries
|
|
2364
|
-
&& visibleLegendSeries.some((entry) => isSeriesToggleable?.(entry) ?? true);
|
|
2365
|
-
// With a drawing tool in hand, [ and ] step through the drawings instead of
|
|
2366
|
-
// the legend, the way a click with that tool picks one.
|
|
2367
|
-
const drawingKeysActive = toolsActive && isDrawingTool(armedTool) && drawings.length > 0 && !keyboardPlacement;
|
|
2368
|
-
// Backspace only deletes what the user is working on; otherwise it is the
|
|
2369
|
-
// pane's back key.
|
|
2370
|
-
const canDeleteDrawing = toolsActive && drawings.length > 0 && (!!selectedDrawingId || isDrawingTool(armedTool));
|
|
2371
|
-
// An owner that holds the cursor decides when it goes, so Esc stays the pane's.
|
|
2372
|
-
const clearableCursor = !!scene?.cursorDate && cursorDate === undefined;
|
|
2373
|
-
|
|
2374
|
-
const placementPanel = (panelId: string) => scene?.panels.find((panel) => panel.id === panelId) ?? null;
|
|
2375
|
-
const startKeyboardPlacement = () => {
|
|
2376
|
-
if (!scene || !armedTool) return;
|
|
2377
|
-
const panel = scene.panels.find((entry) => resolveMeasureAxisDomain(entry) !== null);
|
|
2378
|
-
const domain = panel ? resolveMeasureAxisDomain(panel) : null;
|
|
2379
|
-
const date = keyboardCursorDateRef.current ?? resolveAdjacentCompositeCursorDate(scene, null, -1);
|
|
2380
|
-
if (!panel || !domain || !date) return;
|
|
2381
|
-
const time = date.getTime();
|
|
2382
|
-
const value = resolveMeasureValueAt(panel, time) ?? unprojectCompositeValue(0.5, domain);
|
|
2383
|
-
if (value === null) return;
|
|
2384
|
-
const anchor = { time, value };
|
|
2385
|
-
onActivate?.();
|
|
2386
|
-
if (isDrawingTool(armedTool)) setSelectedDrawingId(null);
|
|
2387
|
-
setKeyboardPlacement({ kind: armedTool, panelId: panel.id, start: anchor, end: anchor, path: [anchor], freeValue: false });
|
|
2388
|
-
keyboardCursorDateRef.current = date;
|
|
2389
|
-
updateCursor(date);
|
|
2390
|
-
};
|
|
2391
|
-
/** Moves the end to a new bar, following the series until Up or Down set a level. */
|
|
2392
|
-
const moveKeyboardPlacement = (
|
|
2393
|
-
placement: KeyboardToolPlacement,
|
|
2394
|
-
move: { time: number } | { level: 1 | -1 },
|
|
2395
|
-
): KeyboardToolPlacement | null => {
|
|
2396
|
-
const panel = placementPanel(placement.panelId);
|
|
2397
|
-
const domain = panel ? resolveMeasureAxisDomain(panel) : null;
|
|
2398
|
-
if (!panel || !domain) return null;
|
|
2399
|
-
let { time, value } = placement.end;
|
|
2400
|
-
let freeValue = placement.freeValue;
|
|
2401
|
-
if ("level" in move) {
|
|
2402
|
-
// Half a row per press: fine enough to reach a wick, quick enough to cross the plot.
|
|
2403
|
-
const step = 1 / (2 * Math.max(panel.height - 1, 1));
|
|
2404
|
-
const ratio = projectCompositeValue(value, domain) ?? 0.5;
|
|
2405
|
-
value = unprojectCompositeValue(Math.max(0, Math.min(1, ratio - move.level * step)), domain) ?? value;
|
|
2406
|
-
freeValue = true;
|
|
2407
|
-
} else {
|
|
2408
|
-
time = move.time;
|
|
2409
|
-
if (!freeValue) value = resolveMeasureValueAt(panel, time) ?? value;
|
|
2410
|
-
}
|
|
2411
|
-
const end = { time, value };
|
|
2412
|
-
return { ...placement, end, path: [...placement.path, end], freeValue };
|
|
2413
|
-
};
|
|
2414
|
-
const finishKeyboardPlacement = () => {
|
|
2415
|
-
const placement = keyboardPlacement;
|
|
2416
|
-
setKeyboardPlacement(null);
|
|
2417
|
-
// A placement that never left its anchor spans nothing, like a click.
|
|
2418
|
-
if (!placement || placement.start.time === placement.end.time) return;
|
|
2419
|
-
if (placement.kind === "zoom") {
|
|
2420
|
-
if (!navigationFrame) return;
|
|
2421
|
-
const range = resolveZoomTimeRange(placement.start.time, placement.end.time, navigationFrame.minimumSpanMs);
|
|
2422
|
-
if (range) setViewportRange(range);
|
|
2423
|
-
return;
|
|
2424
|
-
}
|
|
2425
|
-
if (!isDrawingTool(placement.kind)) return;
|
|
2426
|
-
const points = placement.kind === "pencil"
|
|
2427
|
-
? placement.path.filter((point, index, path) => (
|
|
2428
|
-
index === 0 || point.time !== path[index - 1]!.time || point.value !== path[index - 1]!.value
|
|
2429
|
-
))
|
|
2430
|
-
: [placement.start, placement.end];
|
|
2431
|
-
if (points.length < 2) return;
|
|
2432
|
-
addDrawing({ id: nextDrawingId(), panelId: placement.panelId, points, color: drawColor });
|
|
2433
|
-
};
|
|
2434
|
-
const stepLegend = (direction: -1 | 1) => {
|
|
2435
|
-
onActivate?.();
|
|
2436
|
-
setLegendKeyboardIndex((current) => (
|
|
2437
|
-
current === null
|
|
2438
|
-
? direction > 0 ? 0 : visibleLegendSeries.length - 1
|
|
2439
|
-
: (current + direction + visibleLegendSeries.length) % visibleLegendSeries.length
|
|
2440
|
-
));
|
|
2441
|
-
};
|
|
2442
|
-
const toggleLegendEntry = () => {
|
|
2443
|
-
if (!legendEntry || !legendEntryToggleable) return;
|
|
2444
|
-
onActivate?.();
|
|
2445
|
-
onToggleSeries?.(legendEntry.id);
|
|
2446
|
-
};
|
|
2447
|
-
/** A level at the keyboard cursor's price, or the last price without a cursor. */
|
|
2448
|
-
const addLevelAtCursor = () => {
|
|
2449
|
-
if (!levels?.onEdit || !levelHost || !scene) return;
|
|
2450
|
-
const { panel, domain } = levelHost;
|
|
2451
|
-
const date = keyboardCursorDateRef.current ?? scene.dates.at(-1) ?? null;
|
|
2452
|
-
// The levels' own series, not whichever one the panel lists first.
|
|
2453
|
-
const pricePanel = { ...panel, series: panel.series.filter((entry) => entry.source.id === levels.seriesId) };
|
|
2454
|
-
const value = (date ? resolveMeasureValueAt(pricePanel, date.getTime()) : null)
|
|
2455
|
-
?? (panel.lastPrice?.seriesId === levels.seriesId ? panel.lastPrice.value : null)
|
|
2456
|
-
?? unprojectCompositeValue(0.5, domain);
|
|
2457
|
-
if (value === null) return;
|
|
2458
|
-
const rounded = roundLevelValue(value, domain);
|
|
2459
|
-
onActivate?.();
|
|
2460
|
-
// Enter again at the same cursor picks the level already there.
|
|
2461
|
-
const existing = levelItems.find((level) => level.editable && level.value === rounded);
|
|
2462
|
-
if (existing) {
|
|
2463
|
-
setSelectedLevelId(existing.id);
|
|
2464
|
-
return;
|
|
2465
|
-
}
|
|
2466
|
-
const id = nextLevelId();
|
|
2467
|
-
levels.onEdit({ kind: "add", id, value: rounded });
|
|
2468
|
-
setSelectedLevelId(id);
|
|
2469
|
-
};
|
|
2470
|
-
/** Half a row per press, like a keyboard-placed tool's end. */
|
|
2471
|
-
const nudgeLevel = (level: CompositeChartLevel, direction: 1 | -1) => {
|
|
2472
|
-
if (!levels?.onEdit || !levelHost) return;
|
|
2473
|
-
const { panel, domain } = levelHost;
|
|
2474
|
-
const ratio = projectCompositeValue(level.value, domain);
|
|
2475
|
-
if (ratio === null) return;
|
|
2476
|
-
const step = 1 / (2 * Math.max(panel.height - 1, 1));
|
|
2477
|
-
const value = unprojectCompositeValue(Math.max(0, Math.min(1, ratio - direction * step)), domain);
|
|
2478
|
-
if (value !== null) levels.onEdit({ kind: "move", id: level.id, value: roundLevelValue(value, domain) });
|
|
2479
|
-
};
|
|
2480
|
-
const stepLevel = (direction: -1 | 1) => {
|
|
2481
|
-
// Top to bottom, the order they read in on the plot.
|
|
2482
|
-
const ordered = [...levelItems].sort((left, right) => right.value - left.value);
|
|
2483
|
-
if (ordered.length === 0) return;
|
|
2484
|
-
const index = ordered.findIndex((level) => level.id === selectedLevelId);
|
|
2485
|
-
const next = index < 0
|
|
2486
|
-
? direction > 0 ? 0 : ordered.length - 1
|
|
2487
|
-
: (index + direction + ordered.length) % ordered.length;
|
|
2488
|
-
setSelectedLevelId(ordered[next]!.id);
|
|
2489
|
-
};
|
|
2490
|
-
const removeSelectedLevel = () => {
|
|
2491
|
-
if (!selectedLevel?.editable) return;
|
|
2492
|
-
levels?.onEdit?.({ kind: "remove", id: selectedLevel.id });
|
|
2493
|
-
setSelectedLevelId(null);
|
|
2494
|
-
};
|
|
2495
|
-
const runLevelAction = () => {
|
|
2496
|
-
if (selectedLevel?.actionable) levels?.action?.run(selectedLevel);
|
|
2497
|
-
};
|
|
2498
|
-
const stepDrawing = (direction: -1 | 1) => {
|
|
2499
|
-
if (drawings.length === 0) return;
|
|
2500
|
-
const index = drawings.findIndex((drawing) => drawing.id === selectedDrawingId);
|
|
2501
|
-
const next = index < 0
|
|
2502
|
-
? direction > 0 ? 0 : drawings.length - 1
|
|
2503
|
-
: (index + direction + drawings.length) % drawings.length;
|
|
2504
|
-
setSelectedDrawingId(drawings[next]!.id);
|
|
2505
|
-
};
|
|
2506
|
-
const keyboardToolDrag = useMemo<KeyboardToolDrag | null>(() => {
|
|
2507
|
-
if (!keyboardPlacement || !scene) return null;
|
|
2508
|
-
const panel = scene.panels.find((entry) => entry.id === keyboardPlacement.panelId);
|
|
2509
|
-
const domain = panel ? resolveMeasureAxisDomain(panel) : null;
|
|
2510
|
-
if (!panel || !domain) return null;
|
|
2511
|
-
const project = (point: ChartDrawingPoint) => {
|
|
2512
|
-
const xRatio = projectCompositeTimestamp(scene.timeScale, point.time)?.ratio;
|
|
2513
|
-
const yRatio = projectCompositeValue(point.value, domain);
|
|
2514
|
-
return typeof xRatio === "number" && yRatio !== null ? { xRatio, yRatio } : null;
|
|
2515
|
-
};
|
|
2516
|
-
const start = project(keyboardPlacement.start);
|
|
2517
|
-
const end = project(keyboardPlacement.end);
|
|
2518
|
-
if (!start || !end) return null;
|
|
2519
|
-
return {
|
|
2520
|
-
panelId: panel.id,
|
|
2521
|
-
start: keyboardPlacement.start,
|
|
2522
|
-
end: keyboardPlacement.end,
|
|
2523
|
-
drag: {
|
|
2524
|
-
kind: keyboardPlacement.kind,
|
|
2525
|
-
startXRatio: start.xRatio,
|
|
2526
|
-
startYRatio: start.yRatio,
|
|
2527
|
-
endXRatio: end.xRatio,
|
|
2528
|
-
endYRatio: end.yRatio,
|
|
2529
|
-
path: keyboardPlacement.path.flatMap((point) => {
|
|
2530
|
-
const projected = project(point);
|
|
2531
|
-
return projected ? [projected] : [];
|
|
2532
|
-
}),
|
|
2533
|
-
},
|
|
2534
|
-
};
|
|
2535
|
-
}, [keyboardPlacement, scene]);
|
|
2536
|
-
|
|
2537
|
-
// Scoped, so the chart sees its keys before an older pane or stack handler
|
|
2538
|
-
// (Backspace back, Esc close) whichever mounted first; a dialog opened over
|
|
2539
|
-
// it is a newer scope and still comes first.
|
|
2540
|
-
useShortcut((event) => {
|
|
2541
|
-
if (!keyboardActive) return;
|
|
2542
|
-
const consume = () => {
|
|
2543
|
-
event.preventDefault();
|
|
2544
|
-
event.stopPropagation();
|
|
2545
|
-
};
|
|
2546
|
-
if (timePick && scene && isPlainKey(event, "return", "enter", "escape")) {
|
|
2547
|
-
consume();
|
|
2548
|
-
if (event.name === "escape") timePick.onCancel();
|
|
2549
|
-
else chartActionsRef.current.pick();
|
|
2550
|
-
return;
|
|
2551
|
-
}
|
|
2552
|
-
const levelKeys = toolsActive && armedTool === "level" && levelsEditable && !keyboardPlacement;
|
|
2553
|
-
if (levelKeys && isPlainKey(event, "return", "enter")) {
|
|
2554
|
-
consume();
|
|
2555
|
-
addLevelAtCursor();
|
|
2556
|
-
return;
|
|
2557
|
-
}
|
|
2558
|
-
if (levelKeys && selectedLevel?.editable && isPlainKey(event, "up", "down")) {
|
|
2559
|
-
consume();
|
|
2560
|
-
nudgeLevel(selectedLevel, event.name === "up" ? 1 : -1);
|
|
2561
|
-
return;
|
|
2562
|
-
}
|
|
2563
|
-
if (levelKeys && levelItems.length > 0 && isPlainKey(event, "[", "]")) {
|
|
2564
|
-
consume();
|
|
2565
|
-
stepLevel(event.name === "[" ? -1 : 1);
|
|
2566
|
-
return;
|
|
2567
|
-
}
|
|
2568
|
-
if (toolsActive && selectedLevel?.editable && levelsEditable && isPlainKey(event, "backspace")) {
|
|
2569
|
-
consume();
|
|
2570
|
-
removeSelectedLevel();
|
|
2571
|
-
return;
|
|
2572
|
-
}
|
|
2573
|
-
// Taken even once the level has its alert, which then says so, so a quick
|
|
2574
|
-
// second press never falls through to the plain `a` pan.
|
|
2575
|
-
if (toolsActive && selectedLevel && levels?.action && isShiftedLetter(event, "a")) {
|
|
2576
|
-
consume();
|
|
2577
|
-
levels.action.run(selectedLevel);
|
|
2578
|
-
return;
|
|
2579
|
-
}
|
|
2580
|
-
if (toolsActive && armedTool && isPlainKey(event, "return", "enter") && scene) {
|
|
2581
|
-
consume();
|
|
2582
|
-
if (keyboardPlacement) finishKeyboardPlacement();
|
|
2583
|
-
else startKeyboardPlacement();
|
|
2584
|
-
return;
|
|
2585
|
-
}
|
|
2586
|
-
if (keyboardPlacement && (isPlainKey(event, "escape") || isPlainKey(event, "backspace"))) {
|
|
2587
|
-
consume();
|
|
2588
|
-
setKeyboardPlacement(null);
|
|
2589
|
-
return;
|
|
2590
|
-
}
|
|
2591
|
-
if (keyboardPlacement && keyboardPlacement.kind !== "zoom" && isPlainKey(event, "up", "down")) {
|
|
2592
|
-
consume();
|
|
2593
|
-
const level = event.name === "up" ? 1 : -1;
|
|
2594
|
-
setKeyboardPlacement((current) => current && moveKeyboardPlacement(current, { level }));
|
|
2595
|
-
return;
|
|
2596
|
-
}
|
|
2597
|
-
if (isPlainKey(event, "[", "]") && (drawingKeysActive || legendKeysActive)) {
|
|
2598
|
-
consume();
|
|
2599
|
-
const direction = event.name === "[" ? -1 : 1;
|
|
2600
|
-
if (drawingKeysActive) stepDrawing(direction);
|
|
2601
|
-
else stepLegend(direction);
|
|
2602
|
-
return;
|
|
2603
|
-
}
|
|
2604
|
-
if (isPlainKey(event, "space") && legendKeysActive && legendEntryToggleable) {
|
|
2605
|
-
consume();
|
|
2606
|
-
toggleLegendEntry();
|
|
2607
|
-
return;
|
|
2608
|
-
}
|
|
2609
|
-
if (isPlainKey(event, "escape") && toolsActive && (armedTool || selectedDrawingId || selectedLevelId)) {
|
|
2610
|
-
consume();
|
|
2611
|
-
setArmedTool(null);
|
|
2612
|
-
setSelectedDrawingId(null);
|
|
2613
|
-
setSelectedLevelId(null);
|
|
2614
|
-
return;
|
|
2615
|
-
}
|
|
2616
|
-
if (isPlainKey(event, "escape") && legendKeysActive && legendKeyboardIndex !== null && !clearableCursor) {
|
|
2617
|
-
consume();
|
|
2618
|
-
setLegendKeyboardIndex(null);
|
|
2619
|
-
return;
|
|
2620
|
-
}
|
|
2621
|
-
const interaction = resolveCompositeChartInteraction(event);
|
|
2622
|
-
if (!interaction) return;
|
|
2623
|
-
const cursorInteraction = interaction === "cursor-left"
|
|
2624
|
-
|| interaction === "cursor-right"
|
|
2625
|
-
|| interaction === "clear-cursor";
|
|
2626
|
-
if (!cursorInteraction && !toolsActive) return;
|
|
2627
|
-
if (
|
|
2628
|
-
(interaction === "clear-cursor" && !clearableCursor)
|
|
2629
|
-
|| ((interaction === "arm-measure"
|
|
2630
|
-
|| interaction === "arm-zoom"
|
|
2631
|
-
|| interaction === "arm-line"
|
|
2632
|
-
|| interaction === "arm-pencil") && !scene)
|
|
2633
|
-
|| (interaction === "arm-level" && !levelsEditable)
|
|
2634
|
-
|| (interaction === "delete-drawing" && !canDeleteDrawing)
|
|
2635
|
-
|| (interaction === "cycle-colour" && !isDrawingTool(armedTool) && !selectedDrawingId)
|
|
2636
|
-
// A read-only chart leaves the arrows to a focused tab strip in its pane;
|
|
2637
|
-
// a chart you pan and draw on keeps them, with h/l for the tabs.
|
|
2638
|
-
|| ((interaction === "cursor-left" || interaction === "cursor-right") && (!scene || (arrowsClaimed && !navigable)))
|
|
2639
|
-
|| ((interaction === "zoom-in"
|
|
2640
|
-
|| interaction === "zoom-out"
|
|
2641
|
-
|| interaction === "pan-left"
|
|
2642
|
-
|| interaction === "pan-right") && !navigationFrame)
|
|
2643
|
-
) {
|
|
2644
|
-
return;
|
|
2645
|
-
}
|
|
2646
|
-
consume();
|
|
2647
|
-
switch (interaction) {
|
|
2648
|
-
case "arm-measure":
|
|
2649
|
-
case "arm-zoom":
|
|
2650
|
-
case "arm-line":
|
|
2651
|
-
case "arm-pencil":
|
|
2652
|
-
case "arm-level":
|
|
2653
|
-
onActivate?.();
|
|
2654
|
-
armTool(ARMED_TOOL_BY_INTERACTION[interaction]);
|
|
2655
|
-
return;
|
|
2656
|
-
case "delete-drawing":
|
|
2657
|
-
removeDrawing();
|
|
2658
|
-
return;
|
|
2659
|
-
case "cycle-colour":
|
|
2660
|
-
pickDrawColor(nextDrawingColor(drawColor));
|
|
2661
|
-
return;
|
|
2662
|
-
case "clear-cursor":
|
|
2663
|
-
keyboardCursorDateRef.current = null;
|
|
2664
|
-
updateCursor(null);
|
|
2665
|
-
return;
|
|
2666
|
-
case "cursor-left":
|
|
2667
|
-
case "cursor-right": {
|
|
2668
|
-
const nextDate = resolveAdjacentCompositeCursorDate(
|
|
2669
|
-
scene!,
|
|
2670
|
-
keyboardCursorDateRef.current,
|
|
2671
|
-
interaction === "cursor-left" ? -1 : 1,
|
|
2672
|
-
);
|
|
2673
|
-
keyboardCursorDateRef.current = nextDate;
|
|
2674
|
-
updateCursor(nextDate);
|
|
2675
|
-
if (nextDate && keyboardPlacement) {
|
|
2676
|
-
const time = nextDate.getTime();
|
|
2677
|
-
setKeyboardPlacement((current) => current && moveKeyboardPlacement(current, { time }));
|
|
2678
|
-
}
|
|
2679
|
-
return;
|
|
2680
|
-
}
|
|
2681
|
-
case "pan-left":
|
|
2682
|
-
panViewportByRatio(COMPOSITE_KEYBOARD_PAN_RATIO);
|
|
2683
|
-
return;
|
|
2684
|
-
case "pan-right":
|
|
2685
|
-
panViewportByRatio(-COMPOSITE_KEYBOARD_PAN_RATIO);
|
|
2686
|
-
return;
|
|
2687
|
-
case "reset":
|
|
2688
|
-
resetViewport();
|
|
2689
|
-
return;
|
|
2690
|
-
case "zoom-in":
|
|
2691
|
-
zoomViewport(COMPOSITE_ZOOM_STEP_FACTOR);
|
|
2692
|
-
return;
|
|
2693
|
-
case "zoom-out":
|
|
2694
|
-
zoomViewport(1 / COMPOSITE_ZOOM_STEP_FACTOR);
|
|
2695
|
-
}
|
|
2696
|
-
}, { enabled: keyboardActive, scope: keyboardId });
|
|
2697
|
-
|
|
2698
|
-
// The keys a mode adds show where they act: Enter with a tool in hand, Space
|
|
2699
|
-
// on a legend entry. Everything else the chart answers is in the pane menu.
|
|
2700
|
-
// Footer and menu entries outlive the render that registered them, so they
|
|
2701
|
-
// call through to this render's actions.
|
|
2702
|
-
const chartActions = {
|
|
2703
|
-
pick: () => {
|
|
2704
|
-
const date = keyboardCursorDateRef.current ?? scene?.dates.at(-1) ?? null;
|
|
2705
|
-
if (date) timePick?.onPick(date);
|
|
2706
|
-
},
|
|
2707
|
-
start: armedTool === "level" ? addLevelAtCursor : startKeyboardPlacement,
|
|
2708
|
-
finish: finishKeyboardPlacement,
|
|
2709
|
-
stepLevel,
|
|
2710
|
-
removeLevel: removeSelectedLevel,
|
|
2711
|
-
levelAction: runLevelAction,
|
|
2712
|
-
toggleLegend: toggleLegendEntry,
|
|
2713
|
-
stepLegend,
|
|
2714
|
-
stepDrawing,
|
|
2715
|
-
removeDrawing,
|
|
2716
|
-
cycleColour: () => pickDrawColor(nextDrawingColor(drawColor)),
|
|
2717
|
-
reset: resetViewport,
|
|
2718
|
-
arm: (tool: ChartToolKind) => {
|
|
2719
|
-
onActivate?.();
|
|
2720
|
-
armTool(tool);
|
|
2721
|
-
},
|
|
2722
|
-
};
|
|
2723
|
-
const chartActionsRef = useRef(chartActions);
|
|
2724
|
-
chartActionsRef.current = chartActions;
|
|
2725
|
-
const armedHints = armedTool ? KEYBOARD_TOOL_HINTS[armedTool] : null;
|
|
2726
|
-
const placing = !!keyboardPlacement;
|
|
2727
|
-
const legendEntryVisible = !!legendEntry && visibleSeriesIds.has(legendEntry.id);
|
|
2728
|
-
const resettable = !!activeUserViewport;
|
|
2729
|
-
const timePickLabel = timePick?.label ?? null;
|
|
2730
|
-
const levelActionHint = selectedLevel?.actionable && levels?.action ? levels.action : null;
|
|
2731
|
-
const levelToolKeys = toolsActive && armedTool === "level" && levelsEditable;
|
|
2732
|
-
const canDeleteLevel = toolsActive && levelsEditable && !!selectedLevel?.editable;
|
|
2733
|
-
usePaneFooter(keyboardId, () => {
|
|
2734
|
-
if (!keyboardActive || !scene) return null;
|
|
2735
|
-
const hints: PaneHint[] = [];
|
|
2736
|
-
if (timePickLabel) {
|
|
2737
|
-
hints.push({ id: "chart-pick", key: "Enter", label: timePickLabel, onPress: () => chartActionsRef.current.pick() });
|
|
2738
|
-
} else if (toolsActive && armedHints) {
|
|
2739
|
-
hints.push(placing
|
|
2740
|
-
? { id: "chart-tool", key: "Enter", label: armedHints.finish, title: armedHints.finishTitle, onPress: () => chartActionsRef.current.finish() }
|
|
2741
|
-
: { id: "chart-tool", key: "Enter", label: armedHints.start, title: armedHints.startTitle, onPress: () => chartActionsRef.current.start() });
|
|
2742
|
-
}
|
|
2743
|
-
if (toolsActive && levelActionHint) {
|
|
2744
|
-
hints.push({
|
|
2745
|
-
id: "chart-level-action",
|
|
2746
|
-
key: "Shift+A",
|
|
2747
|
-
label: levelActionHint.label,
|
|
2748
|
-
title: levelActionHint.title,
|
|
2749
|
-
onPress: () => chartActionsRef.current.levelAction(),
|
|
2750
|
-
});
|
|
2751
|
-
}
|
|
2752
|
-
if (legendKeysActive && legendEntryToggleable) {
|
|
2753
|
-
hints.push({
|
|
2754
|
-
id: "chart-series",
|
|
2755
|
-
key: "Space",
|
|
2756
|
-
label: legendEntryVisible ? "hide" : "show",
|
|
2757
|
-
title: legendEntryVisible ? "Hide Series" : "Show Series",
|
|
2758
|
-
onPress: () => chartActionsRef.current.toggleLegend(),
|
|
2759
|
-
});
|
|
2760
|
-
}
|
|
2761
|
-
const menu: ContextMenuItem[] = [];
|
|
2762
|
-
if (toolsActive) {
|
|
2763
|
-
for (const tool of CHART_TOOL_MENU.filter((entry) => entry.kind !== "level" || levelsEditable)) {
|
|
2764
|
-
menu.push({
|
|
2765
|
-
id: `chart-tool-${tool.kind}`,
|
|
2766
|
-
label: tool.label,
|
|
2767
|
-
accelerator: tool.accelerator,
|
|
2768
|
-
checked: armedTool === tool.kind,
|
|
2769
|
-
onSelect: () => chartActionsRef.current.arm(tool.kind),
|
|
2770
|
-
});
|
|
2771
|
-
}
|
|
2772
|
-
if (drawingKeysActive) {
|
|
2773
|
-
menu.push({ id: "chart-drawing-next", label: "Next Drawing", accelerator: "]", onSelect: () => chartActionsRef.current.stepDrawing(1) });
|
|
2774
|
-
}
|
|
2775
|
-
if (canDeleteDrawing) {
|
|
2776
|
-
menu.push({ id: "chart-drawing-delete", label: "Delete Drawing", accelerator: "Backspace", onSelect: () => chartActionsRef.current.removeDrawing() });
|
|
2777
|
-
}
|
|
2778
|
-
if (levelToolKeys && levelItems.length > 0) {
|
|
2779
|
-
menu.push({ id: "chart-level-next", label: "Next Level", accelerator: "]", onSelect: () => chartActionsRef.current.stepLevel(1) });
|
|
2780
|
-
}
|
|
2781
|
-
if (canDeleteLevel) {
|
|
2782
|
-
menu.push({ id: "chart-level-delete", label: "Delete Level", accelerator: "Backspace", onSelect: () => chartActionsRef.current.removeLevel() });
|
|
2783
|
-
}
|
|
2784
|
-
if (isDrawingTool(armedTool) || selectedDrawingId) {
|
|
2785
|
-
menu.push({
|
|
2786
|
-
id: "chart-drawing-colour",
|
|
2787
|
-
label: "Drawing Colour",
|
|
2788
|
-
accelerator: "c",
|
|
2789
|
-
onSelect: () => chartActionsRef.current.cycleColour(),
|
|
2790
|
-
});
|
|
2791
|
-
}
|
|
2792
|
-
if (resettable) menu.push({ id: "chart-reset", label: "Reset Zoom", accelerator: "0", onSelect: () => chartActionsRef.current.reset() });
|
|
2793
|
-
}
|
|
2794
|
-
if (legendKeysActive && anyLegendToggleable && !drawingKeysActive) {
|
|
2795
|
-
menu.push({ id: "chart-series-next", label: "Next Series", accelerator: "]", onSelect: () => chartActionsRef.current.stepLegend(1) });
|
|
2796
|
-
}
|
|
2797
|
-
return { order: 10, hints, menu };
|
|
2798
|
-
}, [
|
|
2799
|
-
armedHints,
|
|
2800
|
-
anyLegendToggleable,
|
|
2801
|
-
armedTool,
|
|
2802
|
-
canDeleteDrawing,
|
|
2803
|
-
drawingKeysActive,
|
|
2804
|
-
keyboardActive,
|
|
2805
|
-
legendEntryToggleable,
|
|
2806
|
-
legendEntryVisible,
|
|
2807
|
-
legendKeysActive,
|
|
2808
|
-
placing,
|
|
2809
|
-
resettable,
|
|
2810
|
-
!!scene,
|
|
2811
|
-
selectedDrawingId,
|
|
2812
|
-
timePickLabel,
|
|
2813
|
-
levelActionHint,
|
|
2814
|
-
levelToolKeys,
|
|
2815
|
-
levelItems.length,
|
|
2816
|
-
canDeleteLevel,
|
|
2817
|
-
levelsEditable,
|
|
2818
|
-
toolsActive,
|
|
2819
|
-
]);
|
|
2820
372
|
|
|
2821
373
|
const leftPadding = leftAxisWidth + (leftAxisWidth ? axisGap : 0);
|
|
2822
374
|
const rightPadding = rightAxisWidth + (rightAxisWidth ? axisGap : 0);
|
|
@@ -2847,7 +399,7 @@ export function CompositeChart({
|
|
|
2847
399
|
accessoryWidth={legendAccessoryWidth}
|
|
2848
400
|
formatValue={formatValue}
|
|
2849
401
|
showLatestChangePercent={showLatestChangePercent}
|
|
2850
|
-
onActivate={
|
|
402
|
+
onActivate={activateFromPointer}
|
|
2851
403
|
onToggleSeries={onToggleSeries}
|
|
2852
404
|
isSeriesToggleable={isSeriesToggleable}
|
|
2853
405
|
keyboardIndex={legendKeyboardIndex}
|
|
@@ -2932,7 +484,7 @@ export function CompositeChart({
|
|
|
2932
484
|
accessoryWidth={legendAccessoryWidth}
|
|
2933
485
|
formatValue={formatValue}
|
|
2934
486
|
showLatestChangePercent={showLatestChangePercent}
|
|
2935
|
-
onActivate={
|
|
487
|
+
onActivate={activateFromPointer}
|
|
2936
488
|
onToggleSeries={onToggleSeries}
|
|
2937
489
|
isSeriesToggleable={isSeriesToggleable}
|
|
2938
490
|
keyboardIndex={legendKeyboardIndex}
|
|
@@ -2948,11 +500,11 @@ export function CompositeChart({
|
|
|
2948
500
|
showColors={isDrawingTool(armedTool) || !!selectedDrawingId}
|
|
2949
501
|
levelTool={levelsEditable}
|
|
2950
502
|
onArmTool={(tool) => {
|
|
2951
|
-
|
|
503
|
+
activateFromPointer();
|
|
2952
504
|
armTool(tool);
|
|
2953
505
|
}}
|
|
2954
506
|
onPickColor={(color) => {
|
|
2955
|
-
|
|
507
|
+
activateFromPointer();
|
|
2956
508
|
pickDrawColor(color);
|
|
2957
509
|
}}
|
|
2958
510
|
/>
|
|
@@ -2987,7 +539,7 @@ export function CompositeChart({
|
|
|
2987
539
|
onDraw={addDrawing}
|
|
2988
540
|
onEditDrawing={editDrawing}
|
|
2989
541
|
onSelectDrawing={setSelectedDrawingId}
|
|
2990
|
-
onActivate={
|
|
542
|
+
onActivate={activateFromPointer}
|
|
2991
543
|
onCursorDateChange={updateCursor}
|
|
2992
544
|
onPanViewport={panViewport}
|
|
2993
545
|
onZoomViewport={zoomViewport}
|