gloomberb 0.10.0 → 0.10.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/package.json +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
- package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
2
|
+
import {
|
|
3
|
+
buildCompositeTimeScale,
|
|
4
|
+
projectCompositeTimestamp,
|
|
5
|
+
unprojectCompositeTimestamp,
|
|
6
|
+
} from "./time-scale";
|
|
2
7
|
|
|
3
8
|
export interface CompositeViewportRange {
|
|
4
9
|
start: Date;
|
|
@@ -44,10 +49,18 @@ function normalizeViewport(
|
|
|
44
49
|
};
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function hasRenderableValue(point: ResolvedSeries["points"][number]): boolean {
|
|
53
|
+
return (
|
|
54
|
+
(typeof point.value === "number" && Number.isFinite(point.value))
|
|
55
|
+
|| (typeof point.close === "number" && Number.isFinite(point.close))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
function pointTimestamps(series: ResolvedSeries[]): number[] {
|
|
48
60
|
const timestamps = new Set<number>();
|
|
49
61
|
for (const entry of series) {
|
|
50
62
|
for (const point of entry.points) {
|
|
63
|
+
if (!hasRenderableValue(point)) continue;
|
|
51
64
|
const timestamp = point.date instanceof Date
|
|
52
65
|
? point.date.getTime()
|
|
53
66
|
: new Date(point.date).getTime();
|
|
@@ -57,6 +70,33 @@ function pointTimestamps(series: ResolvedSeries[]): number[] {
|
|
|
57
70
|
return [...timestamps].sort((left, right) => left - right);
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
function viewportHasTimestamps(
|
|
74
|
+
timestamps: readonly number[],
|
|
75
|
+
viewport: CompositeViewportRange,
|
|
76
|
+
minimumCount: number,
|
|
77
|
+
): boolean {
|
|
78
|
+
const start = finiteTime(viewport.start);
|
|
79
|
+
const end = finiteTime(viewport.end);
|
|
80
|
+
if (start === null || end === null || start > end) return false;
|
|
81
|
+
const required = Math.max(1, Math.floor(minimumCount));
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const timestamp of timestamps) {
|
|
84
|
+
if (timestamp < start) continue;
|
|
85
|
+
if (timestamp > end) break;
|
|
86
|
+
count += 1;
|
|
87
|
+
if (count >= required) return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function compositeViewportHasObservations(
|
|
93
|
+
series: ResolvedSeries[],
|
|
94
|
+
viewport: CompositeViewportRange,
|
|
95
|
+
minimumCount = 2,
|
|
96
|
+
): boolean {
|
|
97
|
+
return viewportHasTimestamps(pointTimestamps(series), viewport, minimumCount);
|
|
98
|
+
}
|
|
99
|
+
|
|
60
100
|
export function resolveCompositeNavigationBounds(
|
|
61
101
|
series: ResolvedSeries[],
|
|
62
102
|
requestedViewport?: CompositeViewportRange | null,
|
|
@@ -70,10 +110,12 @@ export function resolveCompositeNavigationBounds(
|
|
|
70
110
|
});
|
|
71
111
|
if (!requested) return data;
|
|
72
112
|
if (!data) return requested;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
const overlapsData = requested.end.getTime() >= data.start.getTime()
|
|
114
|
+
&& requested.start.getTime() <= data.end.getTime();
|
|
115
|
+
// Loaded series can include a buffer outside the authored viewport. Use the
|
|
116
|
+
// complete real-data extent when the two ranges overlap, but do not clamp a
|
|
117
|
+
// newly authored, disjoint request to stale observations.
|
|
118
|
+
return overlapsData ? data : requested;
|
|
77
119
|
}
|
|
78
120
|
|
|
79
121
|
export function resolveCompositeMinimumSpanMs(
|
|
@@ -83,14 +125,18 @@ export function resolveCompositeMinimumSpanMs(
|
|
|
83
125
|
const start = bounds.start.getTime();
|
|
84
126
|
const end = bounds.end.getTime();
|
|
85
127
|
const timestamps = pointTimestamps(series).filter((timestamp) => timestamp >= start && timestamp <= end);
|
|
86
|
-
|
|
128
|
+
const steps: number[] = [];
|
|
87
129
|
for (let index = 1; index < timestamps.length; index += 1) {
|
|
88
130
|
const step = timestamps[index]! - timestamps[index - 1]!;
|
|
89
|
-
if (step > 0)
|
|
131
|
+
if (step > 0) steps.push(step);
|
|
90
132
|
}
|
|
91
133
|
const boundsSpan = Math.max(end - start, 1);
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
steps.sort((left, right) => left - right);
|
|
135
|
+
// Use an observed upper median rather than the absolute minimum. This keeps
|
|
136
|
+
// one near-duplicate live observation from lowering a daily chart's floor.
|
|
137
|
+
const representativeStep = steps[Math.floor(steps.length / 2)];
|
|
138
|
+
return representativeStep !== undefined
|
|
139
|
+
? Math.min(Math.max(representativeStep, 1), boundsSpan)
|
|
94
140
|
: Math.max(Math.min(boundsSpan, FALLBACK_SINGLE_POINT_SPAN_MS), 1);
|
|
95
141
|
}
|
|
96
142
|
|
|
@@ -98,6 +144,47 @@ function clamp(value: number, minimum: number, maximum: number): number {
|
|
|
98
144
|
return Math.max(minimum, Math.min(maximum, value));
|
|
99
145
|
}
|
|
100
146
|
|
|
147
|
+
function marketViewportProjection(
|
|
148
|
+
viewport: CompositeViewportRange,
|
|
149
|
+
bounds: CompositeViewportRange,
|
|
150
|
+
series: ResolvedSeries[] | undefined,
|
|
151
|
+
): {
|
|
152
|
+
scale: Extract<ReturnType<typeof buildCompositeTimeScale>, { kind: "market" }>;
|
|
153
|
+
startRatio: number;
|
|
154
|
+
endRatio: number;
|
|
155
|
+
} | null {
|
|
156
|
+
if (!series?.some((entry) => entry.timeBasis?.kind === "market")) return null;
|
|
157
|
+
const scale = buildCompositeTimeScale(
|
|
158
|
+
series,
|
|
159
|
+
bounds.start.getTime(),
|
|
160
|
+
bounds.end.getTime(),
|
|
161
|
+
);
|
|
162
|
+
if (scale.kind !== "market") return null;
|
|
163
|
+
const startRatio = projectCompositeTimestamp(scale, viewport.start.getTime())?.ratio;
|
|
164
|
+
const endRatio = projectCompositeTimestamp(scale, viewport.end.getTime())?.ratio;
|
|
165
|
+
if (
|
|
166
|
+
typeof startRatio !== "number"
|
|
167
|
+
|| !Number.isFinite(startRatio)
|
|
168
|
+
|| typeof endRatio !== "number"
|
|
169
|
+
|| !Number.isFinite(endRatio)
|
|
170
|
+
|| startRatio >= endRatio
|
|
171
|
+
) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return { scale, startRatio, endRatio };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function viewportFromMarketRatios(
|
|
178
|
+
scale: Extract<ReturnType<typeof buildCompositeTimeScale>, { kind: "market" }>,
|
|
179
|
+
startRatio: number,
|
|
180
|
+
endRatio: number,
|
|
181
|
+
): CompositeViewportRange {
|
|
182
|
+
return {
|
|
183
|
+
start: new Date(unprojectCompositeTimestamp(scale, startRatio)),
|
|
184
|
+
end: new Date(unprojectCompositeTimestamp(scale, endRatio)),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
101
188
|
export function clampCompositeViewport(
|
|
102
189
|
viewport: CompositeViewportRange,
|
|
103
190
|
bounds: CompositeViewportRange,
|
|
@@ -136,6 +223,7 @@ export function zoomCompositeViewport(
|
|
|
136
223
|
zoomFactor: number,
|
|
137
224
|
anchorRatio: number,
|
|
138
225
|
minimumSpanMs: number,
|
|
226
|
+
series?: ResolvedSeries[],
|
|
139
227
|
): CompositeViewportRange {
|
|
140
228
|
const current = clampCompositeViewport(viewport, bounds);
|
|
141
229
|
if (!Number.isFinite(zoomFactor) || zoomFactor <= 0) return current;
|
|
@@ -144,17 +232,68 @@ export function zoomCompositeViewport(
|
|
|
144
232
|
const end = current.end.getTime();
|
|
145
233
|
const boundsSpan = Math.max(bounds.end.getTime() - bounds.start.getTime(), 1);
|
|
146
234
|
const currentSpan = Math.max(end - start, 1);
|
|
147
|
-
const nextSpan = clamp(
|
|
148
|
-
currentSpan / zoomFactor,
|
|
149
|
-
Math.min(Math.max(minimumSpanMs, 1), boundsSpan),
|
|
150
|
-
boundsSpan,
|
|
151
|
-
);
|
|
152
235
|
const ratio = clamp(anchorRatio, 0, 1);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
236
|
+
const marketProjection = marketViewportProjection(current, bounds, series);
|
|
237
|
+
const candidate = marketProjection
|
|
238
|
+
? (() => {
|
|
239
|
+
const {
|
|
240
|
+
scale,
|
|
241
|
+
startRatio,
|
|
242
|
+
endRatio,
|
|
243
|
+
} = marketProjection;
|
|
244
|
+
const currentMarketSpan = endRatio - startRatio;
|
|
245
|
+
const totalMarketPositions = Math.max(
|
|
246
|
+
scale.endPosition - scale.startPosition,
|
|
247
|
+
Number.EPSILON,
|
|
248
|
+
);
|
|
249
|
+
const minimumMarketSpan = clamp(
|
|
250
|
+
Math.max(minimumSpanMs, 1) / scale.cadenceMs / totalMarketPositions,
|
|
251
|
+
Number.EPSILON,
|
|
252
|
+
1,
|
|
253
|
+
);
|
|
254
|
+
const nextMarketSpan = clamp(
|
|
255
|
+
currentMarketSpan / zoomFactor,
|
|
256
|
+
minimumMarketSpan,
|
|
257
|
+
1,
|
|
258
|
+
);
|
|
259
|
+
const anchor = startRatio + currentMarketSpan * ratio;
|
|
260
|
+
const candidateStart = clamp(
|
|
261
|
+
anchor - nextMarketSpan * ratio,
|
|
262
|
+
0,
|
|
263
|
+
1 - nextMarketSpan,
|
|
264
|
+
);
|
|
265
|
+
return viewportFromMarketRatios(
|
|
266
|
+
scale,
|
|
267
|
+
candidateStart,
|
|
268
|
+
candidateStart + nextMarketSpan,
|
|
269
|
+
);
|
|
270
|
+
})()
|
|
271
|
+
: (() => {
|
|
272
|
+
const nextSpan = clamp(
|
|
273
|
+
currentSpan / zoomFactor,
|
|
274
|
+
Math.min(Math.max(minimumSpanMs, 1), boundsSpan),
|
|
275
|
+
boundsSpan,
|
|
276
|
+
);
|
|
277
|
+
const anchor = start + currentSpan * ratio;
|
|
278
|
+
return clampCompositeViewport({
|
|
279
|
+
start: new Date(anchor - nextSpan * ratio),
|
|
280
|
+
end: new Date(anchor + nextSpan * (1 - ratio)),
|
|
281
|
+
}, bounds);
|
|
282
|
+
})();
|
|
283
|
+
if (!series) return candidate;
|
|
284
|
+
|
|
285
|
+
const timestamps = pointTimestamps(series)
|
|
286
|
+
.filter((timestamp) => timestamp >= bounds.start.getTime() && timestamp <= bounds.end.getTime());
|
|
287
|
+
if (timestamps.length < 2 || viewportHasTimestamps(timestamps, candidate, 2)) {
|
|
288
|
+
return candidate;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const currentHasObservations = viewportHasTimestamps(timestamps, current, 2);
|
|
292
|
+
const candidateSpan = candidate.end.getTime() - candidate.start.getTime();
|
|
293
|
+
// An adaptive response can invalidate an existing viewport. Let zoom-out
|
|
294
|
+
// keep expanding from that state until observations re-enter the window.
|
|
295
|
+
if (!currentHasObservations && candidateSpan > currentSpan) return candidate;
|
|
296
|
+
return current;
|
|
158
297
|
}
|
|
159
298
|
|
|
160
299
|
/**
|
|
@@ -165,15 +304,41 @@ export function panCompositeViewport(
|
|
|
165
304
|
viewport: CompositeViewportRange,
|
|
166
305
|
bounds: CompositeViewportRange,
|
|
167
306
|
shiftRatio: number,
|
|
307
|
+
series?: ResolvedSeries[],
|
|
168
308
|
): CompositeViewportRange {
|
|
169
309
|
const current = clampCompositeViewport(viewport, bounds);
|
|
170
310
|
if (!Number.isFinite(shiftRatio) || shiftRatio === 0) return current;
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
311
|
+
const marketProjection = marketViewportProjection(current, bounds, series);
|
|
312
|
+
const candidate = marketProjection
|
|
313
|
+
? (() => {
|
|
314
|
+
const {
|
|
315
|
+
scale,
|
|
316
|
+
startRatio,
|
|
317
|
+
endRatio,
|
|
318
|
+
} = marketProjection;
|
|
319
|
+
const span = endRatio - startRatio;
|
|
320
|
+
const nextStart = clamp(
|
|
321
|
+
startRatio - span * shiftRatio,
|
|
322
|
+
0,
|
|
323
|
+
1 - span,
|
|
324
|
+
);
|
|
325
|
+
return viewportFromMarketRatios(scale, nextStart, nextStart + span);
|
|
326
|
+
})()
|
|
327
|
+
: (() => {
|
|
328
|
+
const span = Math.max(current.end.getTime() - current.start.getTime(), 1);
|
|
329
|
+
const shift = span * shiftRatio;
|
|
330
|
+
return clampCompositeViewport({
|
|
331
|
+
start: new Date(current.start.getTime() - shift),
|
|
332
|
+
end: new Date(current.end.getTime() - shift),
|
|
333
|
+
}, bounds);
|
|
334
|
+
})();
|
|
335
|
+
if (!series) return candidate;
|
|
336
|
+
|
|
337
|
+
const timestamps = pointTimestamps(series)
|
|
338
|
+
.filter((timestamp) => timestamp >= bounds.start.getTime() && timestamp <= bounds.end.getTime());
|
|
339
|
+
return timestamps.length < 2 || viewportHasTimestamps(timestamps, candidate, 2)
|
|
340
|
+
? candidate
|
|
341
|
+
: current;
|
|
177
342
|
}
|
|
178
343
|
|
|
179
344
|
export function resolveCompositeWheelPanRatio(
|
|
@@ -97,10 +97,6 @@ function median(values: number[]): number | null {
|
|
|
97
97
|
: sorted[middle]!;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function observationGaps(points: CompositeProjectedPoint[], pixelWidth: number): number[] {
|
|
101
|
-
return ratioGaps(points.map((point) => point.xRatio), pixelWidth);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
100
|
function ratioGaps(ratios: readonly number[], pixelWidth: number): number[] {
|
|
105
101
|
const xs = ratios.map((ratio) => ratio * Math.max(pixelWidth - 1, 0)).sort((left, right) => left - right);
|
|
106
102
|
const positiveGaps: number[] = [];
|
|
@@ -111,9 +107,14 @@ function ratioGaps(ratios: readonly number[], pixelWidth: number): number[] {
|
|
|
111
107
|
return positiveGaps;
|
|
112
108
|
}
|
|
113
109
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
export function resolveCompositeObservationWidth(
|
|
111
|
+
points: CompositeProjectedPoint[],
|
|
112
|
+
extent: number,
|
|
113
|
+
minimum: number,
|
|
114
|
+
maximum: number,
|
|
115
|
+
): number {
|
|
116
|
+
const typicalGap = median(ratioGaps(points.map((point) => point.xRatio), extent));
|
|
117
|
+
return clamp(typicalGap === null ? maximum : typicalGap * 0.58, minimum, maximum);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function columnObservationWidth(
|
|
@@ -171,7 +172,8 @@ export function resolveCompositeOhlcWidth(
|
|
|
171
172
|
points: CompositeProjectedPoint[],
|
|
172
173
|
pixelWidth: number,
|
|
173
174
|
): number {
|
|
174
|
-
|
|
175
|
+
const maximum = clamp(pixelWidth * 0.03, 12, 36);
|
|
176
|
+
return resolveCompositeObservationWidth(points, pixelWidth, 2, maximum);
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
function columnPixelGeometry(
|
|
@@ -270,7 +272,6 @@ function drawOhlc(
|
|
|
270
272
|
x + candleWidth / 2,
|
|
271
273
|
Math.max(openY, closeY) + 1,
|
|
272
274
|
candleColor,
|
|
273
|
-
0.88,
|
|
274
275
|
);
|
|
275
276
|
continue;
|
|
276
277
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
|
|
3
|
+
import type { CompositePanelScene } from "./types";
|
|
3
4
|
import {
|
|
4
5
|
renderCompositePanelBitmap,
|
|
5
6
|
resolveCompositeColumnWidth,
|
|
@@ -7,7 +8,10 @@ import {
|
|
|
7
8
|
} from "./rasterizer";
|
|
8
9
|
import { buildCompositeColumnLayout } from "./column-layout";
|
|
9
10
|
import { buildCompositeChartScene } from "./scene";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
renderCompositePanelText,
|
|
13
|
+
resolveCompositeTextOhlcWidth,
|
|
14
|
+
} from "./text-renderer";
|
|
11
15
|
|
|
12
16
|
function point(date: string, value: number): TimeSeriesPoint {
|
|
13
17
|
const observedAt = new Date(`${date}T00:00:00.000Z`);
|
|
@@ -501,6 +505,64 @@ describe("composite chart renderers", () => {
|
|
|
501
505
|
expect(new Set(bitmap.pixels)).not.toEqual(new Set([16, 255]));
|
|
502
506
|
});
|
|
503
507
|
|
|
508
|
+
test("keeps grid lines from showing through candle bodies", () => {
|
|
509
|
+
const source = series("price", "candles", [], "left", "#00ff00");
|
|
510
|
+
const observedAt = new Date("2025-01-01T00:00:00.000Z");
|
|
511
|
+
const candle = {
|
|
512
|
+
date: observedAt,
|
|
513
|
+
observedAt,
|
|
514
|
+
value: 75,
|
|
515
|
+
open: 25,
|
|
516
|
+
high: 90,
|
|
517
|
+
low: 10,
|
|
518
|
+
close: 75,
|
|
519
|
+
};
|
|
520
|
+
source.points = [candle];
|
|
521
|
+
const panel: CompositePanelScene = {
|
|
522
|
+
id: "main",
|
|
523
|
+
height: 9,
|
|
524
|
+
scale: "linear",
|
|
525
|
+
axes: {
|
|
526
|
+
left: {
|
|
527
|
+
side: "left",
|
|
528
|
+
min: 0,
|
|
529
|
+
max: 100,
|
|
530
|
+
scale: "linear",
|
|
531
|
+
unit: "USD",
|
|
532
|
+
unitGroup: "currency",
|
|
533
|
+
seriesIds: [source.id],
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
series: [{
|
|
537
|
+
source,
|
|
538
|
+
points: [{
|
|
539
|
+
point: candle,
|
|
540
|
+
timestamp: observedAt.getTime(),
|
|
541
|
+
value: candle.close,
|
|
542
|
+
xRatio: 0.5,
|
|
543
|
+
yRatio: 0.25,
|
|
544
|
+
breakBefore: true,
|
|
545
|
+
}],
|
|
546
|
+
}],
|
|
547
|
+
};
|
|
548
|
+
const bitmap = renderCompositePanelBitmap(panel, {
|
|
549
|
+
pixelWidth: 101,
|
|
550
|
+
pixelHeight: 101,
|
|
551
|
+
colors: {
|
|
552
|
+
background: "#000000",
|
|
553
|
+
grid: "#ff00ff",
|
|
554
|
+
crosshair: "#ffffff",
|
|
555
|
+
text: "#eeeeee",
|
|
556
|
+
textDim: "#999999",
|
|
557
|
+
negative: "#ff0000",
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
const bodyPixelOffset = (50 * bitmap.width + 47) * 4;
|
|
561
|
+
|
|
562
|
+
expect([...bitmap.pixels.slice(bodyPixelOffset, bodyPixelOffset + 4)])
|
|
563
|
+
.toEqual([0, 255, 0, 255]);
|
|
564
|
+
});
|
|
565
|
+
|
|
504
566
|
test("measures column spacing once per series instead of once per observation", () => {
|
|
505
567
|
const denseColumns = {
|
|
506
568
|
...series("volume", "columns", [], "left"),
|
|
@@ -569,7 +631,51 @@ describe("composite chart renderers", () => {
|
|
|
569
631
|
expect(resolveCompositeOhlcWidth(
|
|
570
632
|
scene.panels[0]!.series[0]!.points,
|
|
571
633
|
2_000,
|
|
572
|
-
)).
|
|
634
|
+
)).toBe(36);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("widens candle bodies as the visible observation spacing grows", () => {
|
|
638
|
+
const candles = series("price", "candles", [], "left");
|
|
639
|
+
candles.points = Array.from({ length: 100 }, (_, index) => {
|
|
640
|
+
const observedAt = new Date(Date.UTC(2025, 0, index + 1));
|
|
641
|
+
return {
|
|
642
|
+
date: observedAt,
|
|
643
|
+
observedAt,
|
|
644
|
+
value: 10,
|
|
645
|
+
open: 8,
|
|
646
|
+
high: 11,
|
|
647
|
+
low: 7,
|
|
648
|
+
close: 10,
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
const fullScene = buildCompositeChartScene(
|
|
652
|
+
[candles],
|
|
653
|
+
[{ id: "main" }],
|
|
654
|
+
{ width: 81, height: 9 },
|
|
655
|
+
)!;
|
|
656
|
+
const zoomedScene = buildCompositeChartScene(
|
|
657
|
+
[candles],
|
|
658
|
+
[{ id: "main" }],
|
|
659
|
+
{
|
|
660
|
+
width: 81,
|
|
661
|
+
height: 9,
|
|
662
|
+
viewport: {
|
|
663
|
+
start: new Date(Date.UTC(2025, 0, 98)),
|
|
664
|
+
end: new Date(Date.UTC(2025, 0, 100)),
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
)!;
|
|
668
|
+
const fullPoints = fullScene.panels[0]!.series[0]!.points;
|
|
669
|
+
const zoomedPoints = zoomedScene.panels[0]!.series[0]!.points;
|
|
670
|
+
|
|
671
|
+
expect(resolveCompositeOhlcWidth(fullPoints, 800)).toBeLessThan(
|
|
672
|
+
resolveCompositeOhlcWidth(zoomedPoints, 800),
|
|
673
|
+
);
|
|
674
|
+
expect(resolveCompositeTextOhlcWidth(fullPoints, 81)).toBe(1);
|
|
675
|
+
expect(resolveCompositeTextOhlcWidth(zoomedPoints, 81)).toBe(5);
|
|
676
|
+
|
|
677
|
+
const zoomedText = renderCompositePanelText(zoomedScene.panels[0]!, 81, null, null).join("\n");
|
|
678
|
+
expect(zoomedText).toContain("█████");
|
|
573
679
|
});
|
|
574
680
|
|
|
575
681
|
test("marks a standalone line observation without extending it through time", () => {
|
|
@@ -397,6 +397,42 @@ describe("composite chart scene", () => {
|
|
|
397
397
|
expect(layout.groupByPoint.get(second)).toMatchObject({ index: 1, count: 2 });
|
|
398
398
|
});
|
|
399
399
|
|
|
400
|
+
test("uses plotted market data when the legend timeline is empty", () => {
|
|
401
|
+
const timestamps = [
|
|
402
|
+
"2025-01-03T15:00:00.000Z",
|
|
403
|
+
"2025-01-03T16:00:00.000Z",
|
|
404
|
+
"2025-01-06T14:00:00.000Z",
|
|
405
|
+
];
|
|
406
|
+
const price = series({
|
|
407
|
+
id: "price",
|
|
408
|
+
timeBasis: {
|
|
409
|
+
kind: "market",
|
|
410
|
+
timeZone: "America/New_York",
|
|
411
|
+
cadenceMs: 60 * 60 * 1_000,
|
|
412
|
+
},
|
|
413
|
+
points: timestamps.map((timestamp, index) => {
|
|
414
|
+
const date = new Date(timestamp);
|
|
415
|
+
return { date, observedAt: date, value: 100 + index };
|
|
416
|
+
}),
|
|
417
|
+
});
|
|
418
|
+
const scene = buildCompositeChartScene(
|
|
419
|
+
[price],
|
|
420
|
+
[{ id: "main" }],
|
|
421
|
+
{
|
|
422
|
+
width: 80,
|
|
423
|
+
height: 10,
|
|
424
|
+
viewport: {
|
|
425
|
+
start: new Date(timestamps[0]!),
|
|
426
|
+
end: new Date(timestamps.at(-1)!),
|
|
427
|
+
},
|
|
428
|
+
timelineSeries: [],
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
expect(scene?.timeScale).toMatchObject({ kind: "market", anchorSeriesId: "price" });
|
|
433
|
+
expect(scene?.panels[0]?.series[0]?.points.map(({ xRatio }) => xRatio)).toEqual([0, 0.5, 1]);
|
|
434
|
+
});
|
|
435
|
+
|
|
400
436
|
test("retains the authored primary market scale when its plotted series is hidden", () => {
|
|
401
437
|
const anchor = series({
|
|
402
438
|
id: "primary-price",
|
|
@@ -27,7 +27,7 @@ function finiteNumber(value: unknown): value is number {
|
|
|
27
27
|
return typeof value === "number" && Number.isFinite(value);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
function resolveTimeSeriesPointValue(point: TimeSeriesPoint): number | null {
|
|
31
31
|
if (finiteNumber(point.value)) return point.value;
|
|
32
32
|
if (finiteNumber(point.close)) return point.close;
|
|
33
33
|
return null;
|
|
@@ -423,8 +423,11 @@ export function buildCompositeChartScene(
|
|
|
423
423
|
const viewport = explicitViewport(options);
|
|
424
424
|
const startTime = viewport?.startTime ?? (firstTime === lastTime ? firstTime - DAY_MS / 2 : firstTime);
|
|
425
425
|
const endTime = viewport?.endTime ?? (firstTime === lastTime ? lastTime + DAY_MS / 2 : lastTime);
|
|
426
|
+
const timelineSeries = options.timelineSeries?.some((entry) => (
|
|
427
|
+
entry.timeBasis?.kind === "market" && entry.points.length > 0
|
|
428
|
+
)) ? options.timelineSeries : dataSeries;
|
|
426
429
|
const timeScale = buildCompositeTimeScale(
|
|
427
|
-
|
|
430
|
+
timelineSeries,
|
|
428
431
|
startTime,
|
|
429
432
|
endTime,
|
|
430
433
|
);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { compositeAxisTicks
|
|
2
|
-
import {
|
|
1
|
+
import { compositeAxisTicks } from "./format";
|
|
2
|
+
import type { CompositeViewportRange } from "./interactions";
|
|
3
|
+
import { resolveCompositeObservationWidth } from "./rasterizer";
|
|
3
4
|
import { buildCompositeColumnLayout, type CompositeColumnLayout } from "./column-layout";
|
|
4
5
|
import { projectCompositeValue } from "./scene";
|
|
6
|
+
import {
|
|
7
|
+
buildCompositeTimeAxisLayout,
|
|
8
|
+
buildCompositeViewportTimeAxisLayout,
|
|
9
|
+
} from "./time-axis";
|
|
5
10
|
import type {
|
|
6
11
|
CompositeAxisDomain,
|
|
7
12
|
CompositeChartScene,
|
|
@@ -159,6 +164,8 @@ function renderOhlc(
|
|
|
159
164
|
width: number,
|
|
160
165
|
height: number,
|
|
161
166
|
): void {
|
|
167
|
+
const candleWidth = resolveCompositeTextOhlcWidth(series.points, width);
|
|
168
|
+
const halfCandleWidth = Math.floor(candleWidth / 2);
|
|
162
169
|
for (const projected of series.points) {
|
|
163
170
|
const source = projected.point;
|
|
164
171
|
const x = cellPoint(projected, width, height).x;
|
|
@@ -173,14 +180,33 @@ function renderOhlc(
|
|
|
173
180
|
if (closeRow === null || highRow === null || lowRow === null) continue;
|
|
174
181
|
drawLine(rows, x, highRow, x, lowRow, "│");
|
|
175
182
|
if (series.source.style === "candles" && openRow !== null) {
|
|
176
|
-
|
|
183
|
+
for (let row = Math.min(openRow, closeRow); row <= Math.max(openRow, closeRow); row += 1) {
|
|
184
|
+
for (let offset = -halfCandleWidth; offset <= halfCandleWidth; offset += 1) {
|
|
185
|
+
setCell(rows, x + offset, row, "█");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
177
188
|
continue;
|
|
178
189
|
}
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
const tickWidth = Math.max(1, halfCandleWidth);
|
|
191
|
+
if (series.source.style === "ohlc" && openRow !== null) {
|
|
192
|
+
drawLine(rows, Math.max(0, x - tickWidth), openRow, x, openRow, "─");
|
|
193
|
+
}
|
|
194
|
+
drawLine(rows, x, closeRow, Math.min(width - 1, x + tickWidth), closeRow, "─");
|
|
181
195
|
}
|
|
182
196
|
}
|
|
183
197
|
|
|
198
|
+
export function resolveCompositeTextOhlcWidth(
|
|
199
|
+
points: CompositeProjectedPoint[],
|
|
200
|
+
width: number,
|
|
201
|
+
): number {
|
|
202
|
+
const maximum = clamp(Math.floor(width / 16), 3, 7);
|
|
203
|
+
const resolved = Math.max(
|
|
204
|
+
1,
|
|
205
|
+
Math.floor(resolveCompositeObservationWidth(points, width, 1, maximum)),
|
|
206
|
+
);
|
|
207
|
+
return resolved % 2 === 0 ? Math.max(1, resolved - 1) : resolved;
|
|
208
|
+
}
|
|
209
|
+
|
|
184
210
|
export function renderCompositePanelText(
|
|
185
211
|
panel: CompositePanelScene,
|
|
186
212
|
width: number,
|
|
@@ -259,27 +285,13 @@ export function renderCompositeAxisText(
|
|
|
259
285
|
return rows;
|
|
260
286
|
}
|
|
261
287
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
288
|
+
export function renderCompositeViewportTimeAxis(
|
|
289
|
+
viewport: CompositeViewportRange,
|
|
290
|
+
width: number,
|
|
291
|
+
): string {
|
|
292
|
+
return buildCompositeViewportTimeAxisLayout(viewport, width).text;
|
|
267
293
|
}
|
|
268
294
|
|
|
269
295
|
export function renderCompositeTimeAxis(scene: CompositeChartScene, width: number): string {
|
|
270
|
-
|
|
271
|
-
const formatDate = (ratio: number) => formatCompositeTimeAxisDate(
|
|
272
|
-
resolveCompositeTimeAxisDate(scene, ratio),
|
|
273
|
-
scene.startTime,
|
|
274
|
-
scene.endTime,
|
|
275
|
-
);
|
|
276
|
-
const startLabel = formatDate(0);
|
|
277
|
-
const midpointLabel = formatDate(0.5);
|
|
278
|
-
const endLabel = formatDate(1);
|
|
279
|
-
placeLabel(chars, startLabel, 0);
|
|
280
|
-
if (chars.length >= startLabel.length + midpointLabel.length + endLabel.length + 4) {
|
|
281
|
-
placeLabel(chars, midpointLabel, (chars.length - 1) / 2);
|
|
282
|
-
}
|
|
283
|
-
placeLabel(chars, endLabel, chars.length - 1);
|
|
284
|
-
return chars.join("");
|
|
296
|
+
return buildCompositeTimeAxisLayout(scene, width).text;
|
|
285
297
|
}
|