gloomberb 0.4.0 → 0.5.0
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 -2
- package/package.json +1 -1
- package/src/app.tsx +184 -69
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +296 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -1
- package/src/components/layout/header.tsx +5 -4
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +263 -32
- package/src/components/layout/shell.tsx +10 -6
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +62 -2
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +82 -5
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +92 -18
- package/src/plugins/builtin/chat-controller.ts +40 -11
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +420 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +33 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/gateway-service.test.ts +22 -0
- package/src/plugins/ibkr/gateway-service.ts +6 -0
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +8 -2
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +129 -14
- package/src/plugins/prediction-markets/controller.ts +1 -1
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/index.tsx +5 -1
- package/src/plugins/prediction-markets/pane.test.tsx +78 -0
- package/src/plugins/prediction-markets/pane.tsx +7 -3
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +17 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
- package/src/plugins/registry.ts +45 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/updater.test.ts +2 -0
- package/src/utils/api-client.ts +12 -0
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RGBA } from "@opentui/core";
|
|
2
|
-
import type { ChartAxisMode, ChartColors, Pixel, PixelBuffer, ChartRenderMode } from "./chart-types";
|
|
2
|
+
import type { ChartAxisMode, ChartColors, ChartIndicatorOverlays, Pixel, PixelBuffer, ChartRenderMode } from "./chart-types";
|
|
3
3
|
import type { ProjectedChartPoint } from "./chart-data";
|
|
4
|
-
import {
|
|
4
|
+
import { formatCompactMarketPriceWithCurrency, formatMarketPrice, resolveAssetDisplayKind } from "../../utils/market-format";
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Styled output types (unchanged public API)
|
|
@@ -26,6 +26,7 @@ export interface StyledContent {
|
|
|
26
26
|
const LAYER_GRID = 0;
|
|
27
27
|
const LAYER_FILL = 1;
|
|
28
28
|
const LAYER_DATA = 2;
|
|
29
|
+
const LAYER_OVERLAY = 2;
|
|
29
30
|
const LAYER_CROSSHAIR = 3;
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
@@ -574,8 +575,21 @@ export function bufferToBrailleLines(buf: PixelBuffer, bgColor: string): StyledC
|
|
|
574
575
|
// Formatting helpers
|
|
575
576
|
// ---------------------------------------------------------------------------
|
|
576
577
|
|
|
577
|
-
export function formatPrice(
|
|
578
|
-
|
|
578
|
+
export function formatPrice(
|
|
579
|
+
value: number,
|
|
580
|
+
assetCategory?: string,
|
|
581
|
+
priceRange?: number,
|
|
582
|
+
precisionOffset = 0,
|
|
583
|
+
minimumFractionDigits = 0,
|
|
584
|
+
fixedFractionDigits?: number,
|
|
585
|
+
): string {
|
|
586
|
+
return formatMarketPrice(value, {
|
|
587
|
+
assetCategory,
|
|
588
|
+
fixedFractionDigits,
|
|
589
|
+
minimumFractionDigits,
|
|
590
|
+
precisionOffset,
|
|
591
|
+
priceRange,
|
|
592
|
+
});
|
|
579
593
|
}
|
|
580
594
|
|
|
581
595
|
function formatPercentAxisValue(value: number): string {
|
|
@@ -585,39 +599,125 @@ function formatPercentAxisValue(value: number): string {
|
|
|
585
599
|
return `${prefix}${value.toFixed(decimals)}%`;
|
|
586
600
|
}
|
|
587
601
|
|
|
588
|
-
|
|
602
|
+
export function formatPriceWithCurrency(
|
|
603
|
+
value: number,
|
|
604
|
+
currency = "USD",
|
|
605
|
+
assetCategory?: string,
|
|
606
|
+
priceRange?: number,
|
|
607
|
+
precisionOffset = 0,
|
|
608
|
+
minimumFractionDigits = 0,
|
|
609
|
+
fixedFractionDigits?: number,
|
|
610
|
+
): string {
|
|
611
|
+
return formatCompactMarketPriceWithCurrency(value, currency, {
|
|
612
|
+
assetCategory,
|
|
613
|
+
fixedFractionDigits,
|
|
614
|
+
minimumFractionDigits,
|
|
615
|
+
precisionOffset,
|
|
616
|
+
priceRange,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
589
619
|
|
|
590
|
-
function
|
|
591
|
-
|
|
592
|
-
|
|
620
|
+
function hasDistinctAxisLabels(labels: string[]): boolean {
|
|
621
|
+
return new Set(labels).size === labels.length;
|
|
622
|
+
}
|
|
593
623
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
return symbol;
|
|
624
|
+
function getAxisFractionDigitFloor(assetCategory: string | undefined, priceRange: number | undefined): number {
|
|
625
|
+
if (priceRange === undefined || !Number.isFinite(priceRange) || priceRange <= 0) return 0;
|
|
626
|
+
|
|
627
|
+
const kind = resolveAssetDisplayKind({ assetCategory });
|
|
628
|
+
const visibleStep = priceRange / 3;
|
|
629
|
+
if (!Number.isFinite(visibleStep) || visibleStep <= 0) return 0;
|
|
630
|
+
|
|
631
|
+
const offset = kind === "cash" ? 0 : 1;
|
|
632
|
+
return Math.max(0, Math.ceil(-Math.log10(visibleStep)) + offset);
|
|
604
633
|
}
|
|
605
634
|
|
|
606
|
-
export function
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
635
|
+
export function resolveAxisFractionDigits(
|
|
636
|
+
prices: number[],
|
|
637
|
+
formatLabel: (value: number, fixedFractionDigits: number) => string,
|
|
638
|
+
minimumFractionDigits = 0,
|
|
639
|
+
maxFractionDigits = 8,
|
|
640
|
+
): number {
|
|
641
|
+
for (let fixedFractionDigits = minimumFractionDigits; fixedFractionDigits <= maxFractionDigits; fixedFractionDigits += 1) {
|
|
642
|
+
const labels = prices.map((value) => formatLabel(value, fixedFractionDigits));
|
|
643
|
+
if (hasDistinctAxisLabels(labels)) return fixedFractionDigits;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return maxFractionDigits;
|
|
647
|
+
}
|
|
610
648
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
649
|
+
export function resolveChartAxisWidth(
|
|
650
|
+
labels: Array<string | null | undefined>,
|
|
651
|
+
minimumWidth: number,
|
|
652
|
+
maximumWidth: number,
|
|
653
|
+
): number {
|
|
654
|
+
if (maximumWidth <= 0) return 0;
|
|
655
|
+
|
|
656
|
+
const longestLabel = labels.reduce((maxWidth, label) => (
|
|
657
|
+
Math.max(maxWidth, label?.length ?? 0)
|
|
658
|
+
), 0);
|
|
659
|
+
|
|
660
|
+
return Math.min(Math.max(longestLabel, minimumWidth), maximumWidth);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function formatAxisCell(label: string | null, width: number): string {
|
|
664
|
+
if (width <= 0) return "";
|
|
665
|
+
if (!label) return " ".repeat(width);
|
|
666
|
+
return label.length >= width ? label.slice(0, width) : label.padStart(width);
|
|
614
667
|
}
|
|
615
668
|
|
|
616
|
-
export function formatAxisValue(
|
|
669
|
+
export function formatAxisValue(
|
|
670
|
+
value: number,
|
|
671
|
+
axisMode: ChartAxisMode,
|
|
672
|
+
basePrice: number,
|
|
673
|
+
currency = "USD",
|
|
674
|
+
assetCategory?: string,
|
|
675
|
+
priceRange?: number,
|
|
676
|
+
fixedFractionDigits?: number,
|
|
677
|
+
): string {
|
|
617
678
|
if (axisMode === "percent" && basePrice !== 0) {
|
|
618
679
|
return formatPercentAxisValue(((value - basePrice) / basePrice) * 100);
|
|
619
680
|
}
|
|
620
|
-
return formatPriceWithCurrency(value, currency);
|
|
681
|
+
return formatPriceWithCurrency(value, currency, assetCategory, priceRange, 0, 0, fixedFractionDigits);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function getCursorAxisMinimumFractionDigits(value: number, assetCategory?: string): number {
|
|
685
|
+
const kind = resolveAssetDisplayKind({ assetCategory });
|
|
686
|
+
|
|
687
|
+
switch (kind) {
|
|
688
|
+
case "cash":
|
|
689
|
+
return 4;
|
|
690
|
+
case "crypto":
|
|
691
|
+
return Math.abs(value) >= 1 ? 4 : 6;
|
|
692
|
+
case "equity":
|
|
693
|
+
case "contract":
|
|
694
|
+
return Math.abs(value) >= 1 ? 2 : 4;
|
|
695
|
+
case "other":
|
|
696
|
+
default:
|
|
697
|
+
return Math.abs(value) >= 1 ? 2 : 4;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function formatCursorAxisValue(
|
|
702
|
+
value: number,
|
|
703
|
+
axisMode: ChartAxisMode,
|
|
704
|
+
basePrice: number,
|
|
705
|
+
currency = "USD",
|
|
706
|
+
assetCategory?: string,
|
|
707
|
+
priceRange?: number,
|
|
708
|
+
): string {
|
|
709
|
+
if (axisMode === "percent" && basePrice !== 0) {
|
|
710
|
+
return formatPercentAxisValue(((value - basePrice) / basePrice) * 100);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return formatPriceWithCurrency(
|
|
714
|
+
value,
|
|
715
|
+
currency,
|
|
716
|
+
assetCategory,
|
|
717
|
+
priceRange,
|
|
718
|
+
2,
|
|
719
|
+
getCursorAxisMinimumFractionDigits(value, assetCategory),
|
|
720
|
+
);
|
|
621
721
|
}
|
|
622
722
|
|
|
623
723
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
@@ -916,8 +1016,10 @@ export interface RenderChartOptions {
|
|
|
916
1016
|
mode: ChartRenderMode;
|
|
917
1017
|
axisMode?: ChartAxisMode;
|
|
918
1018
|
currency?: string;
|
|
1019
|
+
assetCategory?: string;
|
|
919
1020
|
colors: ResolvedChartPalette;
|
|
920
1021
|
timeAxisDates?: Array<Date | string | number>;
|
|
1022
|
+
indicators?: ChartIndicatorOverlays | null;
|
|
921
1023
|
}
|
|
922
1024
|
|
|
923
1025
|
export interface ChartScene {
|
|
@@ -929,6 +1031,7 @@ export interface ChartScene {
|
|
|
929
1031
|
chartRows: number;
|
|
930
1032
|
mode: ChartRenderMode;
|
|
931
1033
|
colors: ResolvedChartPalette;
|
|
1034
|
+
indicators: ChartIndicatorOverlays | null;
|
|
932
1035
|
min: number;
|
|
933
1036
|
max: number;
|
|
934
1037
|
activeIdx: number;
|
|
@@ -958,6 +1061,8 @@ export interface RenderChartResult {
|
|
|
958
1061
|
changePctAtCursor: number | null;
|
|
959
1062
|
cursorColumn: number | null;
|
|
960
1063
|
cursorRow: number | null;
|
|
1064
|
+
axisFractionDigits: number | null;
|
|
1065
|
+
priceRange: number | null;
|
|
961
1066
|
/** Raw pixel buffer for GPU/Kitty rendering path */
|
|
962
1067
|
pixelBuffer: PixelBuffer | null;
|
|
963
1068
|
}
|
|
@@ -988,18 +1093,42 @@ export function getActivePointIndex(
|
|
|
988
1093
|
return bestIndex;
|
|
989
1094
|
}
|
|
990
1095
|
|
|
1096
|
+
function getIndicatorOverlayValues(indicators: ChartIndicatorOverlays | null | undefined): number[] {
|
|
1097
|
+
if (!indicators) return [];
|
|
1098
|
+
|
|
1099
|
+
const values: number[] = [];
|
|
1100
|
+
for (const line of indicators.smaLines) {
|
|
1101
|
+
values.push(...line.points.map((point) => point.value));
|
|
1102
|
+
}
|
|
1103
|
+
for (const line of indicators.emaLines) {
|
|
1104
|
+
values.push(...line.points.map((point) => point.value));
|
|
1105
|
+
}
|
|
1106
|
+
if (indicators.bollinger) {
|
|
1107
|
+
values.push(
|
|
1108
|
+
...indicators.bollinger.upper.map((point) => point.value),
|
|
1109
|
+
...indicators.bollinger.middle.map((point) => point.value),
|
|
1110
|
+
...indicators.bollinger.lower.map((point) => point.value),
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return values.filter((value) => Number.isFinite(value));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
991
1117
|
export function buildChartScene(
|
|
992
1118
|
points: ProjectedChartPoint[],
|
|
993
1119
|
opts: RenderChartOptions,
|
|
994
1120
|
): ChartScene | null {
|
|
995
1121
|
if (points.length === 0) return null;
|
|
996
1122
|
|
|
997
|
-
const
|
|
1123
|
+
const dataMin = opts.mode === "candles" || opts.mode === "ohlc"
|
|
998
1124
|
? Math.min(...points.map((point) => point.low))
|
|
999
1125
|
: Math.min(...points.map((point) => point.close));
|
|
1000
|
-
const
|
|
1126
|
+
const dataMax = opts.mode === "candles" || opts.mode === "ohlc"
|
|
1001
1127
|
? Math.max(...points.map((point) => point.high))
|
|
1002
1128
|
: Math.max(...points.map((point) => point.close));
|
|
1129
|
+
const indicatorValues = getIndicatorOverlayValues(opts.indicators);
|
|
1130
|
+
const min = indicatorValues.length > 0 ? Math.min(dataMin, ...indicatorValues) : dataMin;
|
|
1131
|
+
const max = indicatorValues.length > 0 ? Math.max(dataMax, ...indicatorValues) : dataMax;
|
|
1003
1132
|
const activeIdx = getActivePointIndex(points.length, opts.width, opts.cursorX, opts.mode);
|
|
1004
1133
|
const activePoint = points[activeIdx]!;
|
|
1005
1134
|
const range = max - min || 1;
|
|
@@ -1039,6 +1168,7 @@ export function buildChartScene(
|
|
|
1039
1168
|
chartRows,
|
|
1040
1169
|
mode: opts.mode,
|
|
1041
1170
|
colors: opts.colors,
|
|
1171
|
+
indicators: opts.indicators ?? null,
|
|
1042
1172
|
min,
|
|
1043
1173
|
max,
|
|
1044
1174
|
activeIdx,
|
|
@@ -1057,6 +1187,41 @@ export function buildChartScene(
|
|
|
1057
1187
|
};
|
|
1058
1188
|
}
|
|
1059
1189
|
|
|
1190
|
+
function drawIndicatorOverlays(
|
|
1191
|
+
buf: PixelBuffer,
|
|
1192
|
+
indicators: ChartIndicatorOverlays,
|
|
1193
|
+
pointCount: number,
|
|
1194
|
+
dotTop: number,
|
|
1195
|
+
dotBottom: number,
|
|
1196
|
+
min: number,
|
|
1197
|
+
max: number,
|
|
1198
|
+
mode: ChartRenderMode,
|
|
1199
|
+
): void {
|
|
1200
|
+
const drawOverlay = (overlayPoints: { index: number; value: number }[], color: string) => {
|
|
1201
|
+
for (let i = 0; i < overlayPoints.length - 1; i++) {
|
|
1202
|
+
const p0 = overlayPoints[i]!;
|
|
1203
|
+
const p1 = overlayPoints[i + 1]!;
|
|
1204
|
+
const x0 = getDotX(p0.index, pointCount, buf.width, mode);
|
|
1205
|
+
const y0 = getScaledY(p0.value, min, max, dotTop, dotBottom);
|
|
1206
|
+
const x1 = getDotX(p1.index, pointCount, buf.width, mode);
|
|
1207
|
+
const y1 = getScaledY(p1.value, min, max, dotTop, dotBottom);
|
|
1208
|
+
drawLine(buf, x0, y0, x1, y1, color, LAYER_OVERLAY);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
for (const sma of indicators.smaLines) {
|
|
1213
|
+
drawOverlay(sma.points, sma.color);
|
|
1214
|
+
}
|
|
1215
|
+
for (const ema of indicators.emaLines) {
|
|
1216
|
+
drawOverlay(ema.points, ema.color);
|
|
1217
|
+
}
|
|
1218
|
+
if (indicators.bollinger) {
|
|
1219
|
+
drawOverlay(indicators.bollinger.upper, indicators.bollinger.color);
|
|
1220
|
+
drawOverlay(indicators.bollinger.middle, indicators.bollinger.color);
|
|
1221
|
+
drawOverlay(indicators.bollinger.lower, indicators.bollinger.color);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1060
1225
|
export function renderChart(
|
|
1061
1226
|
points: ProjectedChartPoint[],
|
|
1062
1227
|
opts: RenderChartOptions,
|
|
@@ -1075,6 +1240,8 @@ export function renderChart(
|
|
|
1075
1240
|
changePctAtCursor: null,
|
|
1076
1241
|
cursorColumn: null,
|
|
1077
1242
|
cursorRow: null,
|
|
1243
|
+
axisFractionDigits: null,
|
|
1244
|
+
priceRange: null,
|
|
1078
1245
|
pixelBuffer: null,
|
|
1079
1246
|
};
|
|
1080
1247
|
}
|
|
@@ -1082,6 +1249,7 @@ export function renderChart(
|
|
|
1082
1249
|
const { width, colors: palette, mode } = opts;
|
|
1083
1250
|
const axisMode = opts.axisMode ?? "price";
|
|
1084
1251
|
const currency = opts.currency ?? "USD";
|
|
1252
|
+
const assetCategory = opts.assetCategory;
|
|
1085
1253
|
|
|
1086
1254
|
// Braille resolution: 2 dot-columns per terminal column, 4 dot-rows per terminal row
|
|
1087
1255
|
const dotWidth = width * 2;
|
|
@@ -1094,8 +1262,24 @@ export function renderChart(
|
|
|
1094
1262
|
|
|
1095
1263
|
const buf = createPixelBuffer(dotWidth, totalDotH);
|
|
1096
1264
|
const { min, max } = scene;
|
|
1265
|
+
const priceRange = max - min;
|
|
1097
1266
|
|
|
1098
1267
|
const gridLines = computeGridLines(min, max, 0, chartDotBottom, 3);
|
|
1268
|
+
const axisFractionDigits = axisMode === "price"
|
|
1269
|
+
? resolveAxisFractionDigits(
|
|
1270
|
+
gridLines.map((line) => line.price),
|
|
1271
|
+
(price, fixedFractionDigits) => formatPriceWithCurrency(
|
|
1272
|
+
price,
|
|
1273
|
+
currency,
|
|
1274
|
+
assetCategory,
|
|
1275
|
+
priceRange,
|
|
1276
|
+
0,
|
|
1277
|
+
0,
|
|
1278
|
+
fixedFractionDigits,
|
|
1279
|
+
),
|
|
1280
|
+
getAxisFractionDigitFloor(assetCategory, priceRange),
|
|
1281
|
+
)
|
|
1282
|
+
: null;
|
|
1099
1283
|
drawGridLines(buf, gridLines.map((line) => line.y), palette.gridColor);
|
|
1100
1284
|
|
|
1101
1285
|
switch (mode) {
|
|
@@ -1126,6 +1310,10 @@ export function renderChart(
|
|
|
1126
1310
|
);
|
|
1127
1311
|
}
|
|
1128
1312
|
|
|
1313
|
+
if (opts.indicators) {
|
|
1314
|
+
drawIndicatorOverlays(buf, opts.indicators, points.length, 0, chartDotBottom, min, max, mode);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1129
1317
|
// Cursor mapping stays in terminal-column space
|
|
1130
1318
|
const { activePoint, priceAtCursor, crosshairPrice, dateAtCursor, changeAtCursor, changePctAtCursor } = scene;
|
|
1131
1319
|
|
|
@@ -1137,7 +1325,7 @@ export function renderChart(
|
|
|
1137
1325
|
for (const line of gridLines) {
|
|
1138
1326
|
axisLabelsByRow.set(
|
|
1139
1327
|
Math.min(Math.floor(line.y / 4), Math.max(chartTermRows - 1, 0)),
|
|
1140
|
-
formatAxisValue(line.price, axisMode, points[0]!.close, currency),
|
|
1328
|
+
formatAxisValue(line.price, axisMode, points[0]!.close, currency, assetCategory, priceRange, axisFractionDigits ?? undefined),
|
|
1141
1329
|
);
|
|
1142
1330
|
}
|
|
1143
1331
|
|
|
@@ -1155,6 +1343,8 @@ export function renderChart(
|
|
|
1155
1343
|
changePctAtCursor,
|
|
1156
1344
|
cursorColumn: scene.cursorColumn,
|
|
1157
1345
|
cursorRow: scene.cursorRow,
|
|
1346
|
+
axisFractionDigits,
|
|
1347
|
+
priceRange,
|
|
1158
1348
|
pixelBuffer: buf,
|
|
1159
1349
|
};
|
|
1160
1350
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
CHART_RESOLUTION_STEP_MS,
|
|
3
4
|
clampTimeRangeForResolution,
|
|
4
5
|
getActiveRangePreset,
|
|
6
|
+
getBestSupportedResolutionForDateWindow,
|
|
7
|
+
getBestSupportedResolutionForVisibleWindow,
|
|
5
8
|
getPresetResolution,
|
|
6
9
|
intersectChartResolutions,
|
|
7
10
|
isRangePresetSupported,
|
|
@@ -53,4 +56,68 @@ describe("chart-resolution", () => {
|
|
|
53
56
|
expect(normalizeChartResolution("bogus", "1d")).toBe("1d");
|
|
54
57
|
expect(sortChartResolutions(["1wk", "auto", "15m", "1d"])).toEqual(["auto", "15m", "1d", "1wk"]);
|
|
55
58
|
});
|
|
59
|
+
|
|
60
|
+
test("picks the best supported resolution for the current visible date window", () => {
|
|
61
|
+
const support = [
|
|
62
|
+
{ resolution: "1m", maxRange: "1D" },
|
|
63
|
+
{ resolution: "5m", maxRange: "1W" },
|
|
64
|
+
{ resolution: "15m", maxRange: "1M" },
|
|
65
|
+
{ resolution: "1h", maxRange: "3M" },
|
|
66
|
+
{ resolution: "1d", maxRange: "5Y" },
|
|
67
|
+
{ resolution: "1wk", maxRange: "ALL" },
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
expect(getBestSupportedResolutionForDateWindow({
|
|
71
|
+
start: new Date("2026-01-08T10:00:00Z"),
|
|
72
|
+
end: new Date("2026-01-08T16:00:00Z"),
|
|
73
|
+
}, support)).toBe("1m");
|
|
74
|
+
|
|
75
|
+
expect(getBestSupportedResolutionForDateWindow({
|
|
76
|
+
start: new Date("2026-01-01T00:00:00Z"),
|
|
77
|
+
end: new Date("2026-01-08T00:00:00Z"),
|
|
78
|
+
}, support)).toBe("5m");
|
|
79
|
+
|
|
80
|
+
expect(getBestSupportedResolutionForDateWindow({
|
|
81
|
+
start: new Date("2021-01-01T00:00:00Z"),
|
|
82
|
+
end: new Date("2026-01-01T00:00:00Z"),
|
|
83
|
+
}, support)).toBe("1wk");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("picks a denser supported resolution as the visible window narrows", () => {
|
|
87
|
+
const support = [
|
|
88
|
+
{ resolution: "1m", maxRange: "1D" },
|
|
89
|
+
{ resolution: "5m", maxRange: "1W" },
|
|
90
|
+
{ resolution: "15m", maxRange: "1M" },
|
|
91
|
+
{ resolution: "1h", maxRange: "3M" },
|
|
92
|
+
{ resolution: "1d", maxRange: "5Y" },
|
|
93
|
+
{ resolution: "1wk", maxRange: "ALL" },
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
expect(getBestSupportedResolutionForVisibleWindow({
|
|
97
|
+
start: new Date("2021-01-01T00:00:00Z"),
|
|
98
|
+
end: new Date("2026-01-01T00:00:00Z"),
|
|
99
|
+
}, support, 100)).toBe("1wk");
|
|
100
|
+
|
|
101
|
+
expect(getBestSupportedResolutionForVisibleWindow({
|
|
102
|
+
start: new Date("2025-01-01T00:00:00Z"),
|
|
103
|
+
end: new Date("2026-01-01T00:00:00Z"),
|
|
104
|
+
}, support, 100)).toBe("1d");
|
|
105
|
+
|
|
106
|
+
expect(getBestSupportedResolutionForVisibleWindow({
|
|
107
|
+
start: new Date("2025-12-01T00:00:00Z"),
|
|
108
|
+
end: new Date("2026-01-01T00:00:00Z"),
|
|
109
|
+
}, support, 100)).toBe("1h");
|
|
110
|
+
|
|
111
|
+
expect(getBestSupportedResolutionForVisibleWindow({
|
|
112
|
+
start: new Date("2025-12-25T00:00:00Z"),
|
|
113
|
+
end: new Date("2026-01-01T00:00:00Z"),
|
|
114
|
+
}, support, 100)).toBe("15m");
|
|
115
|
+
|
|
116
|
+
expect(getBestSupportedResolutionForVisibleWindow({
|
|
117
|
+
start: new Date("2026-01-01T00:00:00Z"),
|
|
118
|
+
end: new Date("2026-01-02T00:00:00Z"),
|
|
119
|
+
}, support, 100)).toBe("1m");
|
|
120
|
+
|
|
121
|
+
expect(CHART_RESOLUTION_STEP_MS["1h"]).toBe(60 * 60_000);
|
|
122
|
+
});
|
|
56
123
|
});
|
|
@@ -58,11 +58,34 @@ export const RANGE_PRELOAD_BUFFER: Record<TimeRange, TimeRange> = {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
export const DEFAULT_TICKER_CHART_RANGE_PRESET: TimeRange = "5Y";
|
|
61
|
-
export const DEFAULT_TICKER_CHART_RESOLUTION:
|
|
61
|
+
export const DEFAULT_TICKER_CHART_RESOLUTION: ChartResolution = "auto";
|
|
62
62
|
export const DEFAULT_COMPARISON_CHART_RANGE_PRESET: TimeRange = "1Y";
|
|
63
63
|
export const DEFAULT_COMPARISON_CHART_RESOLUTION: ManualChartResolution = "1d";
|
|
64
64
|
|
|
65
65
|
export const DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS: ManualChartResolution[] = ["1m", "5m", "15m", "1h", "1d", "1wk", "1mo"];
|
|
66
|
+
export const CHART_RESOLUTION_STEP_MS: Record<ManualChartResolution, number> = {
|
|
67
|
+
"1m": 60_000,
|
|
68
|
+
"5m": 5 * 60_000,
|
|
69
|
+
"15m": 15 * 60_000,
|
|
70
|
+
"30m": 30 * 60_000,
|
|
71
|
+
"45m": 45 * 60_000,
|
|
72
|
+
"1h": 60 * 60_000,
|
|
73
|
+
"1d": 24 * 60 * 60_000,
|
|
74
|
+
"1wk": 7 * 24 * 60 * 60_000,
|
|
75
|
+
"1mo": 30 * 24 * 60 * 60_000,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const CHART_RESOLUTION_POINTS_PER_DAY: Record<ManualChartResolution, number> = {
|
|
79
|
+
"1m": 390,
|
|
80
|
+
"5m": 78,
|
|
81
|
+
"15m": 26,
|
|
82
|
+
"30m": 13,
|
|
83
|
+
"45m": 9,
|
|
84
|
+
"1h": 7,
|
|
85
|
+
"1d": 1,
|
|
86
|
+
"1wk": 1 / 5,
|
|
87
|
+
"1mo": 1 / 21,
|
|
88
|
+
};
|
|
66
89
|
|
|
67
90
|
const TIME_RANGE_INDEX = new Map(TIME_RANGE_ORDER.map((range, index) => [range, index]));
|
|
68
91
|
const RANGE_PRESETS_BY_RESOLUTION = TIME_RANGE_ORDER.reduce<Record<ManualChartResolution, TimeRange[]>>((acc, range) => {
|
|
@@ -140,7 +163,7 @@ export function normalizeManualChartResolutions(resolutions: readonly string[]):
|
|
|
140
163
|
|
|
141
164
|
export function intersectChartResolutions(resolutionSets: Array<readonly string[]>): ManualChartResolution[] {
|
|
142
165
|
if (resolutionSets.length === 0) return [];
|
|
143
|
-
const [first, ...rest] = resolutionSets.map((set) => new Set(normalizeManualChartResolutions(set)));
|
|
166
|
+
const [first = new Set<ManualChartResolution>(), ...rest] = resolutionSets.map((set) => new Set(normalizeManualChartResolutions(set)));
|
|
144
167
|
return sortChartResolutions(
|
|
145
168
|
[...first].filter((resolution) => rest.every((set) => set.has(resolution))),
|
|
146
169
|
);
|
|
@@ -194,8 +217,8 @@ export function getSupportMaxRange(
|
|
|
194
217
|
support: readonly ChartResolutionSupport[] | ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
195
218
|
resolution: ManualChartResolution,
|
|
196
219
|
): TimeRange | null {
|
|
197
|
-
if (support
|
|
198
|
-
return support.get(resolution) ?? null;
|
|
220
|
+
if (!Array.isArray(support)) {
|
|
221
|
+
return (support as ReadonlyMap<ManualChartResolution, TimeRange>).get(resolution) ?? null;
|
|
199
222
|
}
|
|
200
223
|
return normalizeChartResolutionSupport(support).find((entry) => entry.resolution === resolution)?.maxRange ?? null;
|
|
201
224
|
}
|
|
@@ -301,6 +324,53 @@ export function getBestSupportedResolutionForPreset(
|
|
|
301
324
|
return supportedResolutions[supportedResolutions.length - 1] ?? null;
|
|
302
325
|
}
|
|
303
326
|
|
|
327
|
+
export function getBestSupportedResolutionForDateWindow(
|
|
328
|
+
window: { start: Date | null; end: Date | null } | null,
|
|
329
|
+
support: readonly ChartResolutionSupport[] | ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
330
|
+
): ManualChartResolution | null {
|
|
331
|
+
if (!window?.start || !window.end) return null;
|
|
332
|
+
const range = getTimeRangeForDateWindow(window);
|
|
333
|
+
return getBestSupportedResolutionForPreset(range, support);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function getTimeRangeForDateWindow(
|
|
337
|
+
window: { start: Date | null; end: Date | null } | null,
|
|
338
|
+
): TimeRange {
|
|
339
|
+
if (!window?.start || !window.end) return "ALL";
|
|
340
|
+
return TIME_RANGE_ORDER.find((candidate) => isDateWindowWithinTimeRange(window.start!, window.end!, candidate)) ?? "ALL";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function getBestSupportedResolutionForVisibleWindow(
|
|
344
|
+
window: { start: Date | null; end: Date | null } | null,
|
|
345
|
+
support: readonly ChartResolutionSupport[] | ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
346
|
+
targetPointCount: number,
|
|
347
|
+
): ManualChartResolution | null {
|
|
348
|
+
if (!window?.start || !window.end) return null;
|
|
349
|
+
|
|
350
|
+
const spanMs = Math.max(window.end.getTime() - window.start.getTime(), 0);
|
|
351
|
+
const dayCount = Math.max(spanMs / CHART_RESOLUTION_STEP_MS["1d"], 1 / 24);
|
|
352
|
+
const supportedResolutions = sortChartResolutions(
|
|
353
|
+
CHART_RESOLUTION_ORDER
|
|
354
|
+
.filter((resolution): resolution is ManualChartResolution => resolution !== "auto")
|
|
355
|
+
.filter((resolution) => {
|
|
356
|
+
const maxRange = getSupportMaxRange(support, resolution);
|
|
357
|
+
return maxRange !== null && isDateWindowWithinTimeRange(window.start!, window.end!, maxRange);
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (supportedResolutions.length === 0) return null;
|
|
362
|
+
|
|
363
|
+
const minimumPointTarget = Math.max(targetPointCount, 1);
|
|
364
|
+
for (const resolution of [...supportedResolutions].reverse()) {
|
|
365
|
+
const estimatedPointCount = Math.max(dayCount * CHART_RESOLUTION_POINTS_PER_DAY[resolution], 1);
|
|
366
|
+
if (estimatedPointCount >= minimumPointTarget) {
|
|
367
|
+
return resolution;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return supportedResolutions[0] ?? null;
|
|
372
|
+
}
|
|
373
|
+
|
|
304
374
|
export function getNextFallbackResolution(
|
|
305
375
|
range: TimeRange,
|
|
306
376
|
currentResolution: ManualChartResolution,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
applyBufferedPanExpansion,
|
|
4
|
+
consumeScrollPanCellDelta,
|
|
5
|
+
consumeScrollPanMovement,
|
|
6
|
+
getDragPanPointDelta,
|
|
7
|
+
getDragPanWindowRatio,
|
|
8
|
+
getKeyboardPanCellCount,
|
|
9
|
+
resolveDragPanOffset,
|
|
10
|
+
resolveHorizontalScrollPanDirection,
|
|
11
|
+
} from "./chart-scroll";
|
|
12
|
+
|
|
13
|
+
describe("chart-scroll", () => {
|
|
14
|
+
test("maps every wheel direction onto horizontal panning", () => {
|
|
15
|
+
expect(resolveHorizontalScrollPanDirection("up")).toBe(1);
|
|
16
|
+
expect(resolveHorizontalScrollPanDirection("left")).toBe(1);
|
|
17
|
+
expect(resolveHorizontalScrollPanDirection("down")).toBe(-1);
|
|
18
|
+
expect(resolveHorizontalScrollPanDirection("right")).toBe(-1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("accumulates wheel input before panning a full cell", () => {
|
|
22
|
+
let result = consumeScrollPanCellDelta(100, 1, 1, 0);
|
|
23
|
+
expect(result.cells).toBe(0);
|
|
24
|
+
expect(result.remainder).toBeCloseTo(0.5);
|
|
25
|
+
|
|
26
|
+
result = consumeScrollPanCellDelta(100, 1, 1, result.remainder);
|
|
27
|
+
expect(result.cells).toBe(1);
|
|
28
|
+
expect(result.remainder).toBeCloseTo(0);
|
|
29
|
+
|
|
30
|
+
result = consumeScrollPanCellDelta(100, 0.4, -1, result.remainder);
|
|
31
|
+
expect(result.cells).toBe(0);
|
|
32
|
+
expect(result.remainder).toBeCloseTo(-0.2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("uses slower pan distances for keyboard and drag input", () => {
|
|
36
|
+
expect(getKeyboardPanCellCount(100)).toBe(2);
|
|
37
|
+
expect(getDragPanPointDelta(10, 100, 200)).toBe(4);
|
|
38
|
+
expect(getDragPanWindowRatio(10, 100)).toBeCloseTo(0.02);
|
|
39
|
+
expect(resolveDragPanOffset(20, 10, 100, 200, 40)).toBe(16);
|
|
40
|
+
expect(resolveDragPanOffset(2, 10, 100, 200, 40)).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("resolves wheel direction, cell delta, remainder, and ratio together", () => {
|
|
44
|
+
const first = consumeScrollPanMovement(100, 1, "up", 0);
|
|
45
|
+
expect(first).toEqual({ cells: 0, remainder: 0.5, ratio: 0 });
|
|
46
|
+
|
|
47
|
+
const second = consumeScrollPanMovement(100, 1, "up", first.remainder);
|
|
48
|
+
expect(second).toEqual({ cells: 1, remainder: 0, ratio: 0.01 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("preserves the active cursor when widening the buffered pan range", () => {
|
|
52
|
+
const next = applyBufferedPanExpansion({
|
|
53
|
+
presetRange: "1W" as const,
|
|
54
|
+
bufferRange: "1M" as const,
|
|
55
|
+
activePreset: "1W" as const,
|
|
56
|
+
panOffset: 12,
|
|
57
|
+
zoomLevel: 2,
|
|
58
|
+
cursorX: 18,
|
|
59
|
+
cursorY: 6,
|
|
60
|
+
}, "3M");
|
|
61
|
+
|
|
62
|
+
expect(next).toMatchObject({
|
|
63
|
+
activePreset: null,
|
|
64
|
+
bufferRange: "3M",
|
|
65
|
+
cursorX: 18,
|
|
66
|
+
cursorY: 6,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|