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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { clearActivePreset } from "./chart-controller";
|
|
2
|
+
import type { TimeRange } from "./chart-types";
|
|
3
|
+
|
|
4
|
+
export type ChartScrollDirection = "up" | "down" | "left" | "right";
|
|
5
|
+
|
|
6
|
+
const KEYBOARD_PAN_WIDTH_RATIO = 0.02;
|
|
7
|
+
const SCROLL_PAN_WIDTH_RATIO = 0.005;
|
|
8
|
+
const DRAG_PAN_VISIBLE_RATIO = 0.2;
|
|
9
|
+
|
|
10
|
+
interface BufferExpansionViewState {
|
|
11
|
+
activePreset: TimeRange | null;
|
|
12
|
+
bufferRange: TimeRange;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScrollPanCellDelta {
|
|
16
|
+
cells: number;
|
|
17
|
+
remainder: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ScrollPanMovement extends ScrollPanCellDelta {
|
|
21
|
+
ratio: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function clamp(value: number, min: number, max: number): number {
|
|
25
|
+
return Math.max(min, Math.min(max, value));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getKeyboardPanCellCount(chartWidth: number): number {
|
|
29
|
+
return Math.max(Math.round(chartWidth * KEYBOARD_PAN_WIDTH_RATIO), 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getDragPanPointDelta(deltaCells: number, chartWidth: number, visibleCount: number): number {
|
|
33
|
+
return Math.round((deltaCells / Math.max(chartWidth, 1)) * Math.max(visibleCount, 1) * DRAG_PAN_VISIBLE_RATIO);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getDragPanWindowRatio(deltaCells: number, chartWidth: number): number {
|
|
37
|
+
return (deltaCells / Math.max(chartWidth, 1)) * DRAG_PAN_VISIBLE_RATIO;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveDragPanOffset(
|
|
41
|
+
startPanOffset: number,
|
|
42
|
+
deltaCells: number,
|
|
43
|
+
chartWidth: number,
|
|
44
|
+
visibleCount: number,
|
|
45
|
+
maxPanOffset: number,
|
|
46
|
+
): number {
|
|
47
|
+
return clamp(
|
|
48
|
+
startPanOffset - getDragPanPointDelta(deltaCells, chartWidth, visibleCount),
|
|
49
|
+
0,
|
|
50
|
+
Math.max(maxPanOffset, 0),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function consumeScrollPanCellDelta(
|
|
55
|
+
chartWidth: number,
|
|
56
|
+
delta: number | undefined,
|
|
57
|
+
direction: 1 | -1,
|
|
58
|
+
remainder: number,
|
|
59
|
+
): ScrollPanCellDelta {
|
|
60
|
+
const rawMagnitude = Math.abs(delta ?? 1);
|
|
61
|
+
const magnitude = rawMagnitude > 0 ? rawMagnitude : 1;
|
|
62
|
+
const next = remainder + direction * Math.max(chartWidth, 1) * SCROLL_PAN_WIDTH_RATIO * magnitude;
|
|
63
|
+
const rawCells = next >= 0 ? Math.floor(next) : Math.ceil(next);
|
|
64
|
+
const cells = Object.is(rawCells, -0) ? 0 : rawCells;
|
|
65
|
+
return {
|
|
66
|
+
cells,
|
|
67
|
+
remainder: next - cells,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function consumeScrollPanMovement(
|
|
72
|
+
chartWidth: number,
|
|
73
|
+
delta: number | undefined,
|
|
74
|
+
scrollDirection: ChartScrollDirection,
|
|
75
|
+
remainder: number,
|
|
76
|
+
): ScrollPanMovement {
|
|
77
|
+
const panDirection = resolveHorizontalScrollPanDirection(scrollDirection);
|
|
78
|
+
const result = consumeScrollPanCellDelta(chartWidth, delta, panDirection, remainder);
|
|
79
|
+
return {
|
|
80
|
+
...result,
|
|
81
|
+
ratio: result.cells / Math.max(chartWidth, 1),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function applyBufferedPanExpansion<S extends BufferExpansionViewState>(
|
|
86
|
+
view: S,
|
|
87
|
+
nextBufferRange: TimeRange,
|
|
88
|
+
): S {
|
|
89
|
+
return {
|
|
90
|
+
...clearActivePreset(view),
|
|
91
|
+
bufferRange: nextBufferRange,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveHorizontalScrollPanDirection(direction: ChartScrollDirection): 1 | -1 {
|
|
96
|
+
switch (direction) {
|
|
97
|
+
case "up":
|
|
98
|
+
case "left":
|
|
99
|
+
return 1;
|
|
100
|
+
case "down":
|
|
101
|
+
case "right":
|
|
102
|
+
return -1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -88,3 +88,15 @@ export interface ComparisonChartViewState {
|
|
|
88
88
|
renderMode?: ComparisonChartRenderMode;
|
|
89
89
|
selectedSymbol: string | null;
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
import type { IndicatorConfig, OverlayPoint, OscillatorPoint, MacdResult, BollingerResult } from "./indicators/types";
|
|
93
|
+
|
|
94
|
+
export interface ChartIndicatorOverlays {
|
|
95
|
+
smaLines: { period: number; points: OverlayPoint[]; color: string }[];
|
|
96
|
+
emaLines: { period: number; points: OverlayPoint[]; color: string }[];
|
|
97
|
+
bollinger: (BollingerResult & { color: string }) | null;
|
|
98
|
+
rsi: OscillatorPoint[] | null;
|
|
99
|
+
macd: MacdResult | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type { IndicatorConfig };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { RIGHT_EDGE_ANCHOR_RATIO, resolveAnchoredChartZoom } from "./chart-viewport";
|
|
2
|
+
import { getVisiblePointCount, RIGHT_EDGE_ANCHOR_RATIO, resolveAnchoredChartZoom } from "./chart-viewport";
|
|
3
3
|
|
|
4
4
|
describe("resolveAnchoredChartZoom", () => {
|
|
5
5
|
test("keeps keyboard zoom pinned to the latest data when anchored to the right edge", () => {
|
|
@@ -22,4 +22,8 @@ describe("resolveAnchoredChartZoom", () => {
|
|
|
22
22
|
panOffset: 63,
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
test("allows zooming down to a two-point viewport", () => {
|
|
27
|
+
expect(getVisiblePointCount(433, 1000)).toBe(2);
|
|
28
|
+
});
|
|
25
29
|
});
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
bucketOhlcSeries,
|
|
4
|
+
getVisibleWindow,
|
|
5
|
+
projectChartData,
|
|
6
|
+
resolveRenderMode,
|
|
7
|
+
resolveStableOhlcProjectionOptions,
|
|
8
|
+
} from "./chart-data";
|
|
3
9
|
import { stepCursorTowards } from "./cursor-motion";
|
|
4
|
-
import { buildTimeAxis, formatAxisValue, renderChart, resolveChartPalette } from "./chart-renderer";
|
|
10
|
+
import { buildChartScene, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./chart-renderer";
|
|
5
11
|
import type { PricePoint } from "../../types/financials";
|
|
6
12
|
import type { ChartRenderMode, ChartViewState } from "./chart-types";
|
|
7
13
|
|
|
@@ -63,6 +69,111 @@ describe("bucketOhlcSeries", () => {
|
|
|
63
69
|
volume: 450,
|
|
64
70
|
});
|
|
65
71
|
});
|
|
72
|
+
|
|
73
|
+
test("keeps interior OHLC buckets aligned to source bars while panning", () => {
|
|
74
|
+
const source = Array.from({ length: 13 }, (_, index) => ({
|
|
75
|
+
date: new Date(Date.UTC(2024, 0, index + 1)),
|
|
76
|
+
open: index,
|
|
77
|
+
high: index + 10,
|
|
78
|
+
low: index - 10,
|
|
79
|
+
close: index + 0.5,
|
|
80
|
+
volume: index + 1,
|
|
81
|
+
})) as PricePoint[];
|
|
82
|
+
|
|
83
|
+
const firstView = bucketOhlcSeries(source.slice(0, 12), 3, { sourceIndexOffset: 0 });
|
|
84
|
+
const pannedView = bucketOhlcSeries(source.slice(1, 13), 3, { sourceIndexOffset: 1 });
|
|
85
|
+
const firstSharedBucket = firstView[1];
|
|
86
|
+
const secondSharedBucket = firstView[2];
|
|
87
|
+
if (!firstSharedBucket || !secondSharedBucket) {
|
|
88
|
+
throw new Error("Expected shared OHLC buckets");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(firstSharedBucket).toEqual({
|
|
92
|
+
date: source[4]!.date,
|
|
93
|
+
open: 1,
|
|
94
|
+
high: 14,
|
|
95
|
+
low: -9,
|
|
96
|
+
close: 4.5,
|
|
97
|
+
volume: 2 + 3 + 4 + 5,
|
|
98
|
+
});
|
|
99
|
+
expect(pannedView[0]).toEqual(firstSharedBucket);
|
|
100
|
+
expect(pannedView[1]).toEqual(secondSharedBucket);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("keeps source-aligned bucket count stable across small pans", () => {
|
|
104
|
+
const source: PricePoint[] = [];
|
|
105
|
+
let cursor = new Date(Date.UTC(2024, 0, 1));
|
|
106
|
+
let sourceIndex = 0;
|
|
107
|
+
while (source.length < 220) {
|
|
108
|
+
const day = cursor.getUTCDay();
|
|
109
|
+
if (day !== 0 && day !== 6) {
|
|
110
|
+
source.push({
|
|
111
|
+
date: new Date(cursor),
|
|
112
|
+
open: sourceIndex,
|
|
113
|
+
high: sourceIndex + 1,
|
|
114
|
+
low: sourceIndex - 1,
|
|
115
|
+
close: sourceIndex + 0.5,
|
|
116
|
+
volume: sourceIndex + 1,
|
|
117
|
+
});
|
|
118
|
+
sourceIndex += 1;
|
|
119
|
+
}
|
|
120
|
+
cursor = new Date(cursor.getTime() + 24 * 3600_000);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const visibleCount = 160;
|
|
124
|
+
const targetWidth = 40;
|
|
125
|
+
const projectionOptions = resolveStableOhlcProjectionOptions({
|
|
126
|
+
pointCount: visibleCount,
|
|
127
|
+
sourceIndexOffset: 0,
|
|
128
|
+
bucketWidth: targetWidth * 2,
|
|
129
|
+
navigationPointCount: visibleCount,
|
|
130
|
+
});
|
|
131
|
+
const targetBucketCount = projectionOptions.ohlcTargetBucketCount!;
|
|
132
|
+
const projectWindow = (start: number) => bucketOhlcSeries(
|
|
133
|
+
source.slice(start, start + visibleCount),
|
|
134
|
+
targetWidth,
|
|
135
|
+
{
|
|
136
|
+
...projectionOptions,
|
|
137
|
+
sourceIndexOffset: start,
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const first = projectWindow(0);
|
|
142
|
+
const pannedOne = projectWindow(1);
|
|
143
|
+
const pannedTwo = projectWindow(2);
|
|
144
|
+
|
|
145
|
+
expect(first).toHaveLength(targetBucketCount);
|
|
146
|
+
expect(pannedOne).toHaveLength(targetBucketCount);
|
|
147
|
+
expect(pannedTwo).toHaveLength(targetBucketCount);
|
|
148
|
+
expect(pannedOne[1]!).toEqual(first[1]!);
|
|
149
|
+
expect(pannedTwo[1]!).toEqual(first[1]!);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("plans stable OHLC buckets from navigation count when histories are comparable", () => {
|
|
153
|
+
expect(resolveStableOhlcProjectionOptions({
|
|
154
|
+
pointCount: 158,
|
|
155
|
+
sourceIndexOffset: 12,
|
|
156
|
+
bucketWidth: 80,
|
|
157
|
+
navigationPointCount: 160,
|
|
158
|
+
})).toMatchObject({
|
|
159
|
+
ohlcBucketWidth: 80,
|
|
160
|
+
ohlcSourceBucketSize: 4,
|
|
161
|
+
ohlcTargetBucketCount: 40,
|
|
162
|
+
sourceIndexOffset: 12,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("plans OHLC buckets from rendered points when navigation count is too different", () => {
|
|
167
|
+
expect(resolveStableOhlcProjectionOptions({
|
|
168
|
+
pointCount: 84,
|
|
169
|
+
sourceIndexOffset: 0,
|
|
170
|
+
bucketWidth: 80,
|
|
171
|
+
navigationPointCount: 160,
|
|
172
|
+
})).toMatchObject({
|
|
173
|
+
ohlcSourceBucketSize: 3,
|
|
174
|
+
ohlcTargetBucketCount: 28,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
66
177
|
});
|
|
67
178
|
|
|
68
179
|
describe("resolveRenderMode", () => {
|
|
@@ -90,17 +201,38 @@ describe("resolveRenderMode", () => {
|
|
|
90
201
|
volume: 1_000 + i,
|
|
91
202
|
})) as PricePoint[];
|
|
92
203
|
|
|
93
|
-
expect(projectChartData(denseSeries, 80, "candles", false).points).toHaveLength(
|
|
94
|
-
expect(projectChartData(denseSeries, 80, "ohlc", false).points).toHaveLength(
|
|
204
|
+
expect(projectChartData(denseSeries, 80, "candles", false).points).toHaveLength(34);
|
|
205
|
+
expect(projectChartData(denseSeries, 80, "ohlc", false).points).toHaveLength(34);
|
|
95
206
|
expect(projectChartData(denseSeries, 80, "line", false).points).toHaveLength(80);
|
|
96
207
|
});
|
|
208
|
+
|
|
209
|
+
test("keeps candle buckets stable when axis width changes the plot by one column", () => {
|
|
210
|
+
const denseSeries = Array.from({ length: 121 }, (_, index) => ({
|
|
211
|
+
date: new Date(Date.UTC(2024, 0, index + 1)),
|
|
212
|
+
open: 100 + index,
|
|
213
|
+
high: 101 + index,
|
|
214
|
+
low: 99 + index,
|
|
215
|
+
close: 100.5 + index,
|
|
216
|
+
volume: 1_000 + index,
|
|
217
|
+
})) as PricePoint[];
|
|
218
|
+
|
|
219
|
+
const wide = projectChartData(denseSeries, 80, "candles", false, { ohlcBucketWidth: 80 }).points;
|
|
220
|
+
const narrowed = projectChartData(denseSeries, 78, "candles", false, { ohlcBucketWidth: 80 }).points;
|
|
221
|
+
|
|
222
|
+
expect(narrowed.map((point) => point.date.toISOString())).toEqual(wide.map((point) => point.date.toISOString()));
|
|
223
|
+
expect(narrowed.map((point) => point.open)).toEqual(wide.map((point) => point.open));
|
|
224
|
+
expect(narrowed.map((point) => point.close)).toEqual(wide.map((point) => point.close));
|
|
225
|
+
});
|
|
97
226
|
});
|
|
98
227
|
|
|
99
228
|
describe("getVisibleWindow", () => {
|
|
100
229
|
test("shows the full selected range at default zoom and lets the renderer downsample it", () => {
|
|
101
230
|
const history = buildDenseHistory(252);
|
|
102
231
|
const viewState: ChartViewState = {
|
|
103
|
-
|
|
232
|
+
presetRange: "1Y",
|
|
233
|
+
bufferRange: "1Y",
|
|
234
|
+
activePreset: "1Y",
|
|
235
|
+
resolution: "auto",
|
|
104
236
|
panOffset: 0,
|
|
105
237
|
zoomLevel: 1,
|
|
106
238
|
cursorX: null,
|
|
@@ -110,14 +242,17 @@ describe("getVisibleWindow", () => {
|
|
|
110
242
|
const window = getVisibleWindow(history, viewState, 80);
|
|
111
243
|
|
|
112
244
|
expect(window.points).toHaveLength(history.length);
|
|
113
|
-
expect(window.points[0]?.date.toISOString()).toBe(history[0]
|
|
114
|
-
expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)
|
|
245
|
+
expect(window.points[0]?.date.toISOString()).toBe(history[0]!.date.toISOString());
|
|
246
|
+
expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)!.date.toISOString());
|
|
115
247
|
});
|
|
116
248
|
|
|
117
249
|
test("zooms into the selected range instead of pinning the default view to chart width", () => {
|
|
118
250
|
const history = buildDenseHistory(252);
|
|
119
251
|
const viewState: ChartViewState = {
|
|
120
|
-
|
|
252
|
+
presetRange: "1Y",
|
|
253
|
+
bufferRange: "1Y",
|
|
254
|
+
activePreset: "1Y",
|
|
255
|
+
resolution: "auto",
|
|
121
256
|
panOffset: 0,
|
|
122
257
|
zoomLevel: 2,
|
|
123
258
|
cursorX: null,
|
|
@@ -127,15 +262,118 @@ describe("getVisibleWindow", () => {
|
|
|
127
262
|
const window = getVisibleWindow(history, viewState, 80);
|
|
128
263
|
|
|
129
264
|
expect(window.points).toHaveLength(126);
|
|
130
|
-
expect(window.points[0]?.date.toISOString()).toBe(history[126]
|
|
131
|
-
expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)
|
|
265
|
+
expect(window.points[0]?.date.toISOString()).toBe(history[126]!.date.toISOString());
|
|
266
|
+
expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)!.date.toISOString());
|
|
132
267
|
});
|
|
133
268
|
});
|
|
134
269
|
|
|
135
270
|
describe("renderChart", () => {
|
|
271
|
+
test("sizes the y-axis to the visible label width instead of a fixed gutter", () => {
|
|
272
|
+
expect(resolveChartAxisWidth(["$18", "$17", "$16", "$14"], 4, 10)).toBe(4);
|
|
273
|
+
expect(resolveChartAxisWidth(["$1.1678", "$1.1654"], 4, 10)).toBe(7);
|
|
274
|
+
expect(resolveChartAxisWidth(["+0.25%", "-0.10%"], 5, 11)).toBe(6);
|
|
275
|
+
});
|
|
276
|
+
|
|
136
277
|
test("formats price axes with the instrument currency", () => {
|
|
137
278
|
expect(formatAxisValue(21_970, "price", 0, "JPY")).toBe("¥22.0K");
|
|
138
279
|
expect(formatAxisValue(12_340, "price", 0, "HKD")).toBe("HK$12.3K");
|
|
280
|
+
expect(formatAxisValue(1.17364, "price", 0, "USD", "CURRENCY")).toBe("$1.17364");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("adapts FX axis precision to the visible price range", () => {
|
|
284
|
+
expect(formatAxisValue(1.167815, "price", 0, "USD", "CURRENCY", 0.12)).toBe("$1.17");
|
|
285
|
+
expect(formatAxisValue(1.167815, "price", 0, "USD", "CURRENCY", 0.0024)).toBe("$1.1678");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("keeps the active price-axis label more precise than coarse grid ticks", () => {
|
|
289
|
+
expect(formatAxisValue(21.184, "price", 0, "USD", "STK", 11)).toBe("$21");
|
|
290
|
+
expect(formatCursorAxisValue(21.184, "price", 0, "USD", "STK", 11)).toBe("$21.18");
|
|
291
|
+
expect(formatCursorAxisValue(1.167815, "price", 0, "USD", "CURRENCY", 0.12)).toBe("$1.1678");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("keeps zoomed equity axis labels distinct and decimal-aware", () => {
|
|
295
|
+
const narrowRangeFixture: PricePoint[] = [
|
|
296
|
+
{ date: new Date("2024-01-02T09:30:00Z"), close: 18.02, volume: 100 },
|
|
297
|
+
{ date: new Date("2024-01-02T09:31:00Z"), close: 17.91, volume: 120 },
|
|
298
|
+
{ date: new Date("2024-01-02T09:32:00Z"), close: 17.84, volume: 140 },
|
|
299
|
+
{ date: new Date("2024-01-02T09:33:00Z"), close: 17.73, volume: 160 },
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const result = renderChart(projectChartData(narrowRangeFixture, 32, "area", false).points, {
|
|
303
|
+
width: 32,
|
|
304
|
+
height: 8,
|
|
305
|
+
showVolume: false,
|
|
306
|
+
volumeHeight: 0,
|
|
307
|
+
cursorX: null,
|
|
308
|
+
cursorY: null,
|
|
309
|
+
mode: "area",
|
|
310
|
+
axisMode: "price",
|
|
311
|
+
currency: "USD",
|
|
312
|
+
assetCategory: "STK",
|
|
313
|
+
colors: palette,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.axisFractionDigits).toBeGreaterThanOrEqual(1);
|
|
317
|
+
expect(new Set(result.axisLabels.map((entry) => entry.label)).size).toBe(result.axisLabels.length);
|
|
318
|
+
expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("includes visible indicator overlays in the chart price range", () => {
|
|
322
|
+
const projection = projectChartData(chartFixture, 12, "line", false);
|
|
323
|
+
const scene = buildChartScene(projection.points, {
|
|
324
|
+
width: 12,
|
|
325
|
+
height: 6,
|
|
326
|
+
showVolume: false,
|
|
327
|
+
volumeHeight: 0,
|
|
328
|
+
cursorX: null,
|
|
329
|
+
cursorY: null,
|
|
330
|
+
mode: projection.effectiveMode,
|
|
331
|
+
colors: palette,
|
|
332
|
+
indicators: {
|
|
333
|
+
smaLines: [{
|
|
334
|
+
period: 2,
|
|
335
|
+
color: "#ff00ff",
|
|
336
|
+
points: [
|
|
337
|
+
{ index: 0, value: 6 },
|
|
338
|
+
{ index: 1, value: 18 },
|
|
339
|
+
],
|
|
340
|
+
}],
|
|
341
|
+
emaLines: [],
|
|
342
|
+
bollinger: null,
|
|
343
|
+
rsi: null,
|
|
344
|
+
macd: null,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(scene?.min).toBe(6);
|
|
349
|
+
expect(scene?.max).toBe(18);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("keeps one decimal on zoomed equity axes even when whole-dollar ticks are distinct", () => {
|
|
353
|
+
const mediumRangeFixture: PricePoint[] = [
|
|
354
|
+
{ date: new Date("2024-01-02T09:30:00Z"), close: 233.82, volume: 100 },
|
|
355
|
+
{ date: new Date("2024-01-02T09:45:00Z"), close: 232.14, volume: 120 },
|
|
356
|
+
{ date: new Date("2024-01-02T10:00:00Z"), close: 230.21, volume: 140 },
|
|
357
|
+
{ date: new Date("2024-01-02T10:15:00Z"), close: 231.67, volume: 160 },
|
|
358
|
+
{ date: new Date("2024-01-02T10:30:00Z"), close: 228.94, volume: 180 },
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const result = renderChart(projectChartData(mediumRangeFixture, 28, "area", false).points, {
|
|
362
|
+
width: 28,
|
|
363
|
+
height: 8,
|
|
364
|
+
showVolume: false,
|
|
365
|
+
volumeHeight: 0,
|
|
366
|
+
cursorX: null,
|
|
367
|
+
cursorY: null,
|
|
368
|
+
mode: "area",
|
|
369
|
+
axisMode: "price",
|
|
370
|
+
currency: "USD",
|
|
371
|
+
assetCategory: "STK",
|
|
372
|
+
colors: palette,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(result.axisFractionDigits).toBe(1);
|
|
376
|
+
expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
|
|
139
377
|
});
|
|
140
378
|
|
|
141
379
|
test("eases coarse cursor motion quickly and settles without overshooting", () => {
|
|
@@ -409,6 +647,7 @@ describe("renderChart", () => {
|
|
|
409
647
|
showVolume: false,
|
|
410
648
|
volumeHeight: 0,
|
|
411
649
|
cursorX: null,
|
|
650
|
+
cursorY: null,
|
|
412
651
|
mode: projection.effectiveMode,
|
|
413
652
|
axisMode: "percent",
|
|
414
653
|
colors: palette,
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { projectComparisonChartData } from "./comparison-chart-data";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildComparisonChartScene,
|
|
5
|
+
formatComparisonAxisValue,
|
|
6
|
+
formatComparisonCursorAxisValue,
|
|
7
|
+
renderComparisonChart,
|
|
8
|
+
} from "./comparison-chart-renderer";
|
|
4
9
|
import type { ComparisonChartSeries } from "./chart-types";
|
|
5
10
|
|
|
6
11
|
function makeSeries(symbol: string, color: string, closes: number[]): ComparisonChartSeries {
|
|
@@ -21,6 +26,71 @@ function textLines(result: ReturnType<typeof renderComparisonChart>): string[] {
|
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
describe("renderComparisonChart", () => {
|
|
29
|
+
test("adapts price-axis precision to the visible range", () => {
|
|
30
|
+
expect(formatComparisonAxisValue(1.167815, "price", 0.12)).toBe("1.17");
|
|
31
|
+
expect(formatComparisonAxisValue(1.167815, "price", 0.0024)).toBe("1.1678");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("keeps the active comparison-axis label more precise than coarse ticks", () => {
|
|
35
|
+
expect(formatComparisonAxisValue(21.184, "price", 11)).toBe("21");
|
|
36
|
+
expect(formatComparisonCursorAxisValue(21.184, "price", 11)).toBe("21.18");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("keeps narrow comparison price axes distinct", () => {
|
|
40
|
+
const projection = projectComparisonChartData([
|
|
41
|
+
makeSeries("AAPL", "#00ff00", [18.02, 17.91, 17.84, 17.73]),
|
|
42
|
+
], 24, {
|
|
43
|
+
timeRange: "ALL",
|
|
44
|
+
panOffset: 0,
|
|
45
|
+
zoomLevel: 1,
|
|
46
|
+
renderMode: "line",
|
|
47
|
+
}, "price");
|
|
48
|
+
|
|
49
|
+
const result = renderComparisonChart(projection, {
|
|
50
|
+
width: 24,
|
|
51
|
+
height: 6,
|
|
52
|
+
cursorX: null,
|
|
53
|
+
cursorY: null,
|
|
54
|
+
selectedSymbol: "AAPL",
|
|
55
|
+
colors: {
|
|
56
|
+
bgColor: "#000000",
|
|
57
|
+
gridColor: "#333333",
|
|
58
|
+
crosshairColor: "#ffffff",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.axisFractionDigits).toBeGreaterThanOrEqual(1);
|
|
63
|
+
expect(new Set(result.axisLabels.map((entry) => entry.label)).size).toBe(result.axisLabels.length);
|
|
64
|
+
expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("keeps one decimal on zoomed comparison price axes even when whole ticks are distinct", () => {
|
|
68
|
+
const projection = projectComparisonChartData([
|
|
69
|
+
makeSeries("AAPL", "#00ff00", [233.82, 232.14, 230.21, 231.67, 228.94]),
|
|
70
|
+
], 24, {
|
|
71
|
+
timeRange: "ALL",
|
|
72
|
+
panOffset: 0,
|
|
73
|
+
zoomLevel: 1,
|
|
74
|
+
renderMode: "line",
|
|
75
|
+
}, "price");
|
|
76
|
+
|
|
77
|
+
const result = renderComparisonChart(projection, {
|
|
78
|
+
width: 24,
|
|
79
|
+
height: 6,
|
|
80
|
+
cursorX: null,
|
|
81
|
+
cursorY: null,
|
|
82
|
+
selectedSymbol: "AAPL",
|
|
83
|
+
colors: {
|
|
84
|
+
bgColor: "#000000",
|
|
85
|
+
gridColor: "#333333",
|
|
86
|
+
crosshairColor: "#ffffff",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.axisFractionDigits).toBe(1);
|
|
91
|
+
expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
24
94
|
test("renders a shared multi-series line chart", () => {
|
|
25
95
|
const projection = projectComparisonChartData([
|
|
26
96
|
makeSeries("AAPL", "#00ff00", [10, 12, 11, 13]),
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
drawGridLines,
|
|
13
13
|
drawLine,
|
|
14
14
|
formatPrice,
|
|
15
|
+
resolveAxisFractionDigits,
|
|
15
16
|
type StyledContent,
|
|
16
17
|
} from "./chart-renderer";
|
|
17
18
|
import type { ChartAxisMode, ComparisonChartRenderMode } from "./chart-types";
|
|
@@ -71,6 +72,8 @@ export interface RenderComparisonChartResult {
|
|
|
71
72
|
crosshairValue: number | null;
|
|
72
73
|
cursorColumn: number | null;
|
|
73
74
|
cursorRow: number | null;
|
|
75
|
+
axisFractionDigits: number | null;
|
|
76
|
+
priceRange: number | null;
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function clamp(value: number, min: number, max: number): number {
|
|
@@ -95,10 +98,32 @@ function formatPercentAxisValue(value: number): string {
|
|
|
95
98
|
return `${prefix}${value.toFixed(decimals)}%`;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
function getComparisonAxisFractionDigitFloor(priceRange: number | undefined): number {
|
|
102
|
+
if (priceRange === undefined || !Number.isFinite(priceRange) || priceRange <= 0) return 0;
|
|
103
|
+
const visibleStep = priceRange / 3;
|
|
104
|
+
if (!Number.isFinite(visibleStep) || visibleStep <= 0) return 0;
|
|
105
|
+
return Math.max(0, Math.ceil(-Math.log10(visibleStep)) + 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatComparisonAxisValue(
|
|
109
|
+
value: number,
|
|
110
|
+
axisMode: ChartAxisMode,
|
|
111
|
+
priceRange?: number,
|
|
112
|
+
fixedFractionDigits?: number,
|
|
113
|
+
): string {
|
|
114
|
+
return axisMode === "percent"
|
|
115
|
+
? formatPercentAxisValue(value)
|
|
116
|
+
: formatPrice(value, undefined, priceRange, 0, 0, fixedFractionDigits);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatComparisonCursorAxisValue(
|
|
120
|
+
value: number,
|
|
121
|
+
axisMode: ChartAxisMode,
|
|
122
|
+
priceRange?: number,
|
|
123
|
+
): string {
|
|
99
124
|
return axisMode === "percent"
|
|
100
125
|
? formatPercentAxisValue(value)
|
|
101
|
-
: formatPrice(value);
|
|
126
|
+
: formatPrice(value, undefined, priceRange, 2, Math.abs(value) >= 1 ? 2 : 4);
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
function fillColumn(
|
|
@@ -305,13 +330,23 @@ export function renderComparisonChart(
|
|
|
305
330
|
crosshairValue: null,
|
|
306
331
|
cursorColumn: null,
|
|
307
332
|
cursorRow: null,
|
|
333
|
+
axisFractionDigits: null,
|
|
334
|
+
priceRange: null,
|
|
308
335
|
};
|
|
309
336
|
}
|
|
310
337
|
|
|
311
338
|
const dotWidth = scene.width * 2;
|
|
312
339
|
const chartDotBottom = scene.chartRows * 4 - 1;
|
|
340
|
+
const priceRange = scene.max - scene.min;
|
|
313
341
|
const buf = createPixelBuffer(dotWidth, scene.height * 4);
|
|
314
342
|
const gridLines = computeGridLines(scene.min, scene.max, 0, chartDotBottom, 3);
|
|
343
|
+
const axisFractionDigits = scene.axisMode === "price"
|
|
344
|
+
? resolveAxisFractionDigits(
|
|
345
|
+
gridLines.map((line) => line.price),
|
|
346
|
+
(price, fixedFractionDigits) => formatComparisonAxisValue(price, scene.axisMode, priceRange, fixedFractionDigits),
|
|
347
|
+
getComparisonAxisFractionDigitFloor(priceRange),
|
|
348
|
+
)
|
|
349
|
+
: null;
|
|
315
350
|
drawGridLines(buf, gridLines.map((line) => line.y), scene.colors.gridColor);
|
|
316
351
|
|
|
317
352
|
const selectedSeries = getSelectedSeries(scene.series, scene.selectedSymbol);
|
|
@@ -336,7 +371,7 @@ export function renderComparisonChart(
|
|
|
336
371
|
for (const line of gridLines) {
|
|
337
372
|
axisLabelsByRow.set(
|
|
338
373
|
Math.min(Math.floor(line.y / 4), Math.max(scene.chartRows - 1, 0)),
|
|
339
|
-
formatComparisonAxisValue(line.price, scene.axisMode),
|
|
374
|
+
formatComparisonAxisValue(line.price, scene.axisMode, priceRange, axisFractionDigits ?? undefined),
|
|
340
375
|
);
|
|
341
376
|
}
|
|
342
377
|
|
|
@@ -350,5 +385,7 @@ export function renderComparisonChart(
|
|
|
350
385
|
crosshairValue: scene.crosshairValue,
|
|
351
386
|
cursorColumn: scene.cursorColumn,
|
|
352
387
|
cursorRow: scene.cursorRow,
|
|
388
|
+
axisFractionDigits,
|
|
389
|
+
priceRange,
|
|
353
390
|
};
|
|
354
391
|
}
|