gloomberb 0.10.3 → 0.10.4
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 +3 -3
- package/README.zh-CN.md +2 -2
- package/package.json +2 -1
- package/src/api-client/auth.ts +8 -0
- package/src/api-client/index.ts +70 -0
- package/src/api-client/types.ts +49 -0
- package/src/app/runtime/desktop-deeplink.test.ts +8 -0
- package/src/app/runtime/desktop-deeplink.ts +14 -1
- package/src/components/chart/composite/composite-chart.test.tsx +340 -17
- package/src/components/chart/composite/composite-chart.tsx +859 -68
- package/src/components/chart/composite/interactions.test.ts +78 -22
- package/src/components/chart/composite/interactions.ts +45 -17
- package/src/components/chart/composite/rasterizer.ts +8 -2
- package/src/components/chart/composite/renderers.test.ts +61 -1
- package/src/components/chart/composite/scene.ts +14 -4
- package/src/components/chart/composite/tools.test.ts +371 -0
- package/src/components/chart/composite/tools.ts +508 -0
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/native/raster/primitives.ts +18 -0
- package/src/components/chart/price-axis-labels.tsx +42 -2
- package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
- package/src/components/command-bar/action-runtime.ts +3 -0
- package/src/components/command-bar/assist/inventory.test.ts +93 -0
- package/src/components/command-bar/assist/inventory.ts +92 -0
- package/src/components/command-bar/assist/model.test.ts +104 -0
- package/src/components/command-bar/assist/model.ts +169 -0
- package/src/components/command-bar/assist/runtime.ts +185 -0
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
- package/src/components/command-bar/list/model.ts +15 -2
- package/src/components/command-bar/list/navigation.ts +6 -0
- package/src/components/command-bar/list/view.tsx +16 -3
- package/src/components/command-bar/pane-templates/workflow.ts +1 -0
- package/src/components/command-bar/panel/body.tsx +2 -0
- package/src/components/command-bar/panel/index.tsx +3 -0
- package/src/components/command-bar/panel/runtime.ts +6 -0
- package/src/components/command-bar/panel/types.ts +1 -0
- package/src/components/command-bar/routes/root/results.ts +28 -2
- package/src/components/command-bar/routes/root/runtime.ts +66 -36
- package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
- package/src/components/command-bar/routing/navigation-state.ts +17 -0
- package/src/components/command-bar/selection-runtime.ts +26 -0
- package/src/components/command-bar/surface/assist.test.tsx +276 -0
- package/src/components/command-bar/surface/index.tsx +93 -2
- package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
- package/src/components/command-bar/view-model.ts +7 -1
- package/src/components/command-bar/workflow/broker.test.ts +22 -0
- package/src/components/command-bar/workflow/broker.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +40 -2
- package/src/components/layout/floating-pane.tsx +4 -1
- package/src/components/layout/pane/header.tsx +64 -6
- package/src/components/layout/pane/index.tsx +4 -1
- package/src/components/layout/shell/index.tsx +23 -0
- package/src/components/layout/shell/pane/layers.tsx +5 -0
- package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
- package/src/components/onboarding/account-step/form-panel.tsx +157 -0
- package/src/components/onboarding/account-step/index.tsx +58 -0
- package/src/components/onboarding/account-step/model.test.ts +67 -0
- package/src/components/onboarding/account-step/model.ts +126 -0
- package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
- package/src/components/onboarding/onboarding-steps.tsx +91 -55
- package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
- package/src/components/onboarding/onboarding-wizard.tsx +93 -9
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
- package/src/components/onboarding/wizard-account.ts +235 -0
- package/src/components/onboarding/wizard-keyboard.ts +67 -1
- package/src/components/onboarding/wizard-model.ts +2 -2
- package/src/i18n/es.ts +1093 -0
- package/src/i18n/index.test.ts +5 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/ja.ts +64 -3
- package/src/i18n/ko.ts +64 -3
- package/src/i18n/languages.test.ts +4 -1
- package/src/i18n/languages.ts +12 -1
- package/src/i18n/zh-cn.ts +64 -3
- package/src/i18n/zh-tw.ts +64 -3
- package/src/market-data/coordinator/quotes.ts +63 -1
- package/src/market-data/coordinator/subscriptions.test.ts +33 -0
- package/src/market-data/quotes/cadence.ts +2 -0
- package/src/plugins/builtin/account-management/footer.ts +9 -3
- package/src/plugins/builtin/account-management/model.ts +53 -1
- package/src/plugins/builtin/account-management/pane.tsx +107 -24
- package/src/plugins/builtin/ai/index.tsx +7 -2
- package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
- package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
- package/src/plugins/builtin/chart-composer/index.tsx +10 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
- package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
- package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
- package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
- package/src/plugins/builtin/cloud/plugin.tsx +9 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
- package/src/plugins/builtin/help/index.tsx +38 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
- package/src/plugins/builtin/options/footer.test.ts +20 -124
- package/src/plugins/builtin/options/footer.ts +32 -90
- package/src/plugins/builtin/options/index.tsx +6 -0
- package/src/plugins/builtin/options/view.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
- package/src/plugins/builtin/shared/live-streaming.ts +60 -0
- package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
- package/src/plugins/builtin/shared/plan-access.ts +109 -0
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +12 -0
- package/src/plugins/registry/pane-settings.ts +29 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
- package/src/renderers/electrobun/view/host/box.tsx +3 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
- package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
- package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
- package/src/renderers/electrobun/view/host/input.tsx +16 -1
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/opentui/chart-surface.tsx +2 -0
- package/src/renderers/opentui/host.tsx +3 -0
- package/src/sources/gloomberb-cloud/index.test.ts +8 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -4
- package/src/sources/provider-router/history.test.ts +3 -3
- package/src/sources/provider-router/history.ts +15 -4
- package/src/sources/yahoo-finance/history.test.ts +47 -0
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/state/hooks/quote-streaming.test.tsx +124 -1
- package/src/state/hooks/quote-streaming.ts +90 -7
- package/src/theme/colors.ts +20 -1
- package/src/theme/themes.test.ts +3 -0
- package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
- package/src/time-series/resolution.test.ts +51 -0
- package/src/time-series/resolution.ts +41 -0
- package/src/time-series/resolve.test.ts +207 -1
- package/src/time-series/resolve.ts +196 -29
- package/src/time-series/spec-studies.test.ts +4 -0
- package/src/time-series/spec.ts +15 -1
- package/src/time-series/types.ts +7 -0
- package/src/time-series/use-chart-resolution.test.tsx +37 -0
- package/src/time-series/use-chart-resolution.ts +17 -1
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +17 -0
|
@@ -11,11 +11,7 @@ import {
|
|
|
11
11
|
zoomCompositeViewport,
|
|
12
12
|
type CompositeViewportRange,
|
|
13
13
|
} from "./interactions";
|
|
14
|
-
import {
|
|
15
|
-
buildCompositeTimeScale,
|
|
16
|
-
projectCompositeTimestamp,
|
|
17
|
-
} from "./time-scale";
|
|
18
|
-
|
|
14
|
+
import { buildCompositeTimeScale, projectCompositeTimestamp } from "./time-scale";
|
|
19
15
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
20
16
|
|
|
21
17
|
function point(day: number): TimeSeriesPoint {
|
|
@@ -69,6 +65,18 @@ function intradayMarketSeries(): ResolvedSeries {
|
|
|
69
65
|
};
|
|
70
66
|
}
|
|
71
67
|
|
|
68
|
+
function marketSlotSpan(
|
|
69
|
+
data: ResolvedSeries,
|
|
70
|
+
bounds: CompositeViewportRange,
|
|
71
|
+
view: CompositeViewportRange,
|
|
72
|
+
): number {
|
|
73
|
+
const scale = buildCompositeTimeScale([data], bounds.start.getTime(), bounds.end.getTime());
|
|
74
|
+
const start = projectCompositeTimestamp(scale, view.start.getTime())!.ratio;
|
|
75
|
+
const end = projectCompositeTimestamp(scale, view.end.getTime())!.ratio;
|
|
76
|
+
const positions = scale.kind === "market" ? scale.endPosition - scale.startPosition : 1;
|
|
77
|
+
return (end - start) * positions;
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
describe("composite chart interactions", () => {
|
|
73
81
|
test("zooms around the right edge and clamps zoom-out to loaded bounds", () => {
|
|
74
82
|
const bounds = viewport(1, 11);
|
|
@@ -91,27 +99,21 @@ describe("composite chart interactions", () => {
|
|
|
91
99
|
expect(clampedNewer).toEqual(viewport(7, 11));
|
|
92
100
|
});
|
|
93
101
|
|
|
94
|
-
test("
|
|
102
|
+
test("never changes the visible slot count while panning across a closed session", () => {
|
|
95
103
|
const data = intradayMarketSeries();
|
|
96
104
|
const bounds = resolveCompositeNavigationBounds([data])!;
|
|
97
|
-
|
|
105
|
+
let visible: CompositeViewportRange = {
|
|
98
106
|
start: new Date("2025-01-03T13:30:00.000Z"),
|
|
99
107
|
end: new Date("2025-01-03T17:00:00.000Z"),
|
|
100
108
|
};
|
|
101
|
-
const
|
|
102
|
-
const scale = buildCompositeTimeScale(
|
|
103
|
-
[data],
|
|
104
|
-
bounds.start.getTime(),
|
|
105
|
-
bounds.end.getTime(),
|
|
106
|
-
);
|
|
107
|
-
const projectedSpan = (window: CompositeViewportRange) => (
|
|
108
|
-
projectCompositeTimestamp(scale, window.end.getTime())!.ratio
|
|
109
|
-
- projectCompositeTimestamp(scale, window.start.getTime())!.ratio
|
|
110
|
-
);
|
|
109
|
+
const slots = marketSlotSpan(data, bounds, visible);
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
// A wall-clock shift changes how many market slots stay in view, which
|
|
112
|
+
// renders as the plot squeezing shut instead of scrolling sideways.
|
|
113
|
+
for (let step = 0; step < 4; step += 1) {
|
|
114
|
+
visible = panCompositeViewport(visible, bounds, 0.5, [data]);
|
|
115
|
+
expect(marketSlotSpan(data, bounds, visible)).toBeCloseTo(slots, 6);
|
|
116
|
+
}
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
test("uses representative cadence instead of a stray fine gap as the zoom floor", () => {
|
|
@@ -126,11 +128,49 @@ describe("composite chart interactions", () => {
|
|
|
126
128
|
expect(resolveCompositeNavigationBounds([data], viewport(5, 9))).toEqual(viewport(1, 9));
|
|
127
129
|
});
|
|
128
130
|
|
|
129
|
-
test("
|
|
131
|
+
test("reserves an older unloaded window without extending into the future", () => {
|
|
132
|
+
const data = series([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
133
|
+
const requested = viewport(5, 9);
|
|
134
|
+
const bounds = resolveCompositeNavigationBounds(
|
|
135
|
+
[data],
|
|
136
|
+
requested,
|
|
137
|
+
{ historicalPaddingRatio: 1 },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(bounds).toEqual(viewport(-3, 9));
|
|
141
|
+
expect(panCompositeViewport(requested, bounds!, 1, [data], true))
|
|
142
|
+
.toEqual(viewport(1, 5));
|
|
143
|
+
expect(panCompositeViewport(requested, bounds!, -10, [data], true))
|
|
144
|
+
.toEqual(viewport(5, 9));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("enters unloaded history from a full market-session viewport without changing span", () => {
|
|
148
|
+
const data = intradayMarketSeries();
|
|
149
|
+
const requested = {
|
|
150
|
+
start: data.points[0]!.date,
|
|
151
|
+
end: data.points.at(-1)!.date,
|
|
152
|
+
};
|
|
153
|
+
const bounds = resolveCompositeNavigationBounds(
|
|
154
|
+
[data],
|
|
155
|
+
requested,
|
|
156
|
+
{ historicalPaddingRatio: 1 },
|
|
157
|
+
)!;
|
|
158
|
+
|
|
159
|
+
const older = panCompositeViewport(requested, bounds, 1, [data], true);
|
|
160
|
+
expect(older).not.toEqual(requested);
|
|
161
|
+
expect(older.end.getTime()).toBe(requested.start.getTime());
|
|
162
|
+
expect(marketSlotSpan(data, bounds, older))
|
|
163
|
+
.toBeCloseTo(marketSlotSpan(data, bounds, requested), 6);
|
|
164
|
+
expect(panCompositeViewport(older, bounds, -1, [data], true)).toEqual(requested);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("clamps overlapping requested bounds to real data and keeps disjoint ones reachable", () => {
|
|
130
168
|
const data = series([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
131
169
|
|
|
132
170
|
expect(resolveCompositeNavigationBounds([data], viewport(5, 12))).toEqual(viewport(1, 9));
|
|
133
|
-
|
|
171
|
+
// Bounds equal to an observation-free window would strand pan and zoom
|
|
172
|
+
// inside it, so a disjoint request stays unioned with the loaded data.
|
|
173
|
+
expect(resolveCompositeNavigationBounds([data], viewport(20, 30))).toEqual(viewport(1, 30));
|
|
134
174
|
});
|
|
135
175
|
|
|
136
176
|
test("requires distinct renderable observations inside a viewport", () => {
|
|
@@ -187,6 +227,22 @@ describe("composite chart interactions", () => {
|
|
|
187
227
|
expect(resolveCompositeChartInteraction({ name: "-", targetEditable: true })).toBeNull();
|
|
188
228
|
});
|
|
189
229
|
|
|
230
|
+
test("arms chart tools from either shifted key encoding", () => {
|
|
231
|
+
// Kitty keyboard protocol reports the modifier; legacy terminals only send
|
|
232
|
+
// the uppercase letter.
|
|
233
|
+
expect(resolveCompositeChartInteraction({ name: "m", shift: true })).toBe("arm-measure");
|
|
234
|
+
expect(resolveCompositeChartInteraction({ name: "M", sequence: "M" })).toBe("arm-measure");
|
|
235
|
+
expect(resolveCompositeChartInteraction({ name: "z", shift: true })).toBe("arm-zoom");
|
|
236
|
+
expect(resolveCompositeChartInteraction({ name: "Z", sequence: "Z" })).toBe("arm-zoom");
|
|
237
|
+
expect(resolveCompositeChartInteraction({ name: "d", shift: true })).toBe("arm-line");
|
|
238
|
+
expect(resolveCompositeChartInteraction({ name: "P", sequence: "P" })).toBe("arm-pencil");
|
|
239
|
+
expect(resolveCompositeChartInteraction({ name: "backspace" })).toBe("delete-drawing");
|
|
240
|
+
expect(resolveCompositeChartInteraction({ name: "c" })).toBe("cycle-colour");
|
|
241
|
+
// Unshifted m and z stay with the pane that already owns them.
|
|
242
|
+
expect(resolveCompositeChartInteraction({ name: "m" })).toBeNull();
|
|
243
|
+
expect(resolveCompositeChartInteraction({ name: "z" })).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
|
|
190
246
|
test("maps wheel directions to bounded horizontal pan increments", () => {
|
|
191
247
|
expect(resolveCompositeWheelPanRatio("up", 1)).toBe(0.005);
|
|
192
248
|
expect(resolveCompositeWheelPanRatio("left", 100)).toBe(0.04);
|
|
@@ -11,11 +11,17 @@ export interface CompositeViewportRange {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export type CompositeChartInteraction =
|
|
14
|
+
| "arm-line"
|
|
15
|
+
| "arm-measure"
|
|
16
|
+
| "arm-pencil"
|
|
17
|
+
| "arm-zoom"
|
|
18
|
+
| "cycle-colour"
|
|
14
19
|
| "clear-cursor"
|
|
15
20
|
| "cursor-left"
|
|
16
21
|
| "cursor-right"
|
|
17
22
|
| "pan-left"
|
|
18
23
|
| "pan-right"
|
|
24
|
+
| "delete-drawing"
|
|
19
25
|
| "reset"
|
|
20
26
|
| "zoom-in"
|
|
21
27
|
| "zoom-out";
|
|
@@ -100,6 +106,7 @@ export function compositeViewportHasObservations(
|
|
|
100
106
|
export function resolveCompositeNavigationBounds(
|
|
101
107
|
series: ResolvedSeries[],
|
|
102
108
|
requestedViewport?: CompositeViewportRange | null,
|
|
109
|
+
options: { historicalPaddingRatio?: number } = {},
|
|
103
110
|
): CompositeViewportRange | null {
|
|
104
111
|
const requested = normalizeViewport(requestedViewport);
|
|
105
112
|
const timestamps = pointTimestamps(series);
|
|
@@ -113,9 +120,22 @@ export function resolveCompositeNavigationBounds(
|
|
|
113
120
|
const overlapsData = requested.end.getTime() >= data.start.getTime()
|
|
114
121
|
&& requested.start.getTime() <= data.end.getTime();
|
|
115
122
|
// Loaded series can include a buffer outside the authored viewport. Use the
|
|
116
|
-
// complete real-data extent when the two ranges overlap
|
|
117
|
-
//
|
|
118
|
-
|
|
123
|
+
// complete real-data extent when the two ranges overlap. A disjoint request
|
|
124
|
+
// keeps its own window, unioned with the data so navigation can always get
|
|
125
|
+
// back: bounds equal to an observation-free window strand pan and zoom.
|
|
126
|
+
if (!overlapsData) {
|
|
127
|
+
return {
|
|
128
|
+
start: new Date(Math.min(data.start.getTime(), requested.start.getTime())),
|
|
129
|
+
end: new Date(Math.max(data.end.getTime(), requested.end.getTime())),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const paddingRatio = Math.max(options.historicalPaddingRatio ?? 0, 0);
|
|
133
|
+
if (paddingRatio === 0) return data;
|
|
134
|
+
const requestedSpan = Math.max(requested.end.getTime() - requested.start.getTime(), 1);
|
|
135
|
+
return {
|
|
136
|
+
start: new Date(Math.min(data.start.getTime(), requested.start.getTime()) - requestedSpan * paddingRatio),
|
|
137
|
+
end: data.end,
|
|
138
|
+
};
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
export function resolveCompositeMinimumSpanMs(
|
|
@@ -305,34 +325,32 @@ export function panCompositeViewport(
|
|
|
305
325
|
bounds: CompositeViewportRange,
|
|
306
326
|
shiftRatio: number,
|
|
307
327
|
series?: ResolvedSeries[],
|
|
328
|
+
allowEmpty = false,
|
|
308
329
|
): CompositeViewportRange {
|
|
309
330
|
const current = clampCompositeViewport(viewport, bounds);
|
|
310
331
|
if (!Number.isFinite(shiftRatio) || shiftRatio === 0) return current;
|
|
311
332
|
const marketProjection = marketViewportProjection(current, bounds, series);
|
|
333
|
+
// Market scales collapse overnight and weekend gaps, so a wall-clock shift
|
|
334
|
+
// changes how many slots stay visible and the plot accordions instead of
|
|
335
|
+
// translating. Shift in slot space so the visible span is preserved.
|
|
312
336
|
const candidate = marketProjection
|
|
313
337
|
? (() => {
|
|
314
|
-
const {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const nextStart = clamp(
|
|
321
|
-
startRatio - span * shiftRatio,
|
|
322
|
-
0,
|
|
323
|
-
1 - span,
|
|
338
|
+
const { scale, startRatio, endRatio } = marketProjection;
|
|
339
|
+
const marketSpan = endRatio - startRatio;
|
|
340
|
+
const nextStart = clamp(startRatio - marketSpan * shiftRatio, 0, 1 - marketSpan);
|
|
341
|
+
return clampCompositeViewport(
|
|
342
|
+
viewportFromMarketRatios(scale, nextStart, nextStart + marketSpan),
|
|
343
|
+
bounds,
|
|
324
344
|
);
|
|
325
|
-
return viewportFromMarketRatios(scale, nextStart, nextStart + span);
|
|
326
345
|
})()
|
|
327
346
|
: (() => {
|
|
328
|
-
const
|
|
329
|
-
const shift = span * shiftRatio;
|
|
347
|
+
const shift = Math.max(current.end.getTime() - current.start.getTime(), 1) * shiftRatio;
|
|
330
348
|
return clampCompositeViewport({
|
|
331
349
|
start: new Date(current.start.getTime() - shift),
|
|
332
350
|
end: new Date(current.end.getTime() - shift),
|
|
333
351
|
}, bounds);
|
|
334
352
|
})();
|
|
335
|
-
if (!series) return candidate;
|
|
353
|
+
if (!series || allowEmpty) return candidate;
|
|
336
354
|
|
|
337
355
|
const timestamps = pointTimestamps(series)
|
|
338
356
|
.filter((timestamp) => timestamp >= bounds.start.getTime() && timestamp <= bounds.end.getTime());
|
|
@@ -403,6 +421,16 @@ export function resolveCompositeChartInteraction(event: {
|
|
|
403
421
|
const key = (name || sequence).toLowerCase();
|
|
404
422
|
if (key === "left") return event.shift ? "pan-left" : "cursor-left";
|
|
405
423
|
if (key === "right") return event.shift ? "pan-right" : "cursor-right";
|
|
424
|
+
// Terminals reserve shift-drag and option-drag for their own selection, so the
|
|
425
|
+
// pointer tools also need a keyboard path that always reaches the app. Without
|
|
426
|
+
// the kitty keyboard protocol a shifted letter arrives uppercase and unflagged.
|
|
427
|
+
const shifted = event.shift || name === name.toUpperCase() && name !== key;
|
|
428
|
+
if (shifted && key === "m") return "arm-measure";
|
|
429
|
+
if (shifted && key === "z") return "arm-zoom";
|
|
430
|
+
if (shifted && key === "d") return "arm-line";
|
|
431
|
+
if (shifted && key === "p") return "arm-pencil";
|
|
432
|
+
if (key === "backspace") return "delete-drawing";
|
|
433
|
+
if (key === "c" && !event.shift) return "cycle-colour";
|
|
406
434
|
if (event.shift) return null;
|
|
407
435
|
if (key === "a") return "pan-left";
|
|
408
436
|
if (key === "d") return "pan-right";
|
|
@@ -2,6 +2,7 @@ import type { NativeChartBitmap } from "../native/chart-rasterizer";
|
|
|
2
2
|
import {
|
|
3
3
|
drawCircle,
|
|
4
4
|
drawLine,
|
|
5
|
+
fillOpaque,
|
|
5
6
|
fillRect,
|
|
6
7
|
parseHex,
|
|
7
8
|
type RgbaColor,
|
|
@@ -250,7 +251,12 @@ function drawOhlc(
|
|
|
250
251
|
const candleWidth = resolveCompositeOhlcWidth(series.points, width);
|
|
251
252
|
for (const projected of series.points) {
|
|
252
253
|
const source = projected.point;
|
|
253
|
-
const
|
|
254
|
+
const halfWidth = Math.min(candleWidth / 2, Math.max(width - 1, 0) / 2);
|
|
255
|
+
const x = clamp(
|
|
256
|
+
pixelPoint(projected, width, height).x,
|
|
257
|
+
halfWidth,
|
|
258
|
+
Math.max(width - 1 - halfWidth, halfWidth),
|
|
259
|
+
);
|
|
254
260
|
const close = source.close ?? projected.value;
|
|
255
261
|
const open = source.open ?? close;
|
|
256
262
|
const high = source.high ?? Math.max(open, close);
|
|
@@ -292,7 +298,7 @@ export function renderCompositePanelBitmap(
|
|
|
292
298
|
const background = parseHex(options.colors.background);
|
|
293
299
|
const grid = parseHex(options.colors.grid);
|
|
294
300
|
const negative = parseHex(options.colors.negative);
|
|
295
|
-
|
|
301
|
+
fillOpaque(data, background);
|
|
296
302
|
|
|
297
303
|
for (let index = 1; index <= 3; index += 1) {
|
|
298
304
|
const y = (height - 1) * (index / 4);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveCompositeOhlcWidth,
|
|
8
8
|
} from "./rasterizer";
|
|
9
9
|
import { buildCompositeColumnLayout } from "./column-layout";
|
|
10
|
-
import { buildCompositeChartScene } from "./scene";
|
|
10
|
+
import { buildCompositeChartScene, projectCompositeValue } from "./scene";
|
|
11
11
|
import {
|
|
12
12
|
renderCompositePanelText,
|
|
13
13
|
resolveCompositeTextOhlcWidth,
|
|
@@ -678,6 +678,66 @@ describe("composite chart renderers", () => {
|
|
|
678
678
|
expect(zoomedText).toContain("█████");
|
|
679
679
|
});
|
|
680
680
|
|
|
681
|
+
test("keeps the first and last candle bodies fully inside the bitmap", () => {
|
|
682
|
+
const candles = series("price", "candles", [], "left", "#00ff66");
|
|
683
|
+
candles.points = ["2025-01-01", "2025-01-02", "2025-01-03"].map((date) => ({
|
|
684
|
+
...point(date, 10),
|
|
685
|
+
open: 8,
|
|
686
|
+
high: 13,
|
|
687
|
+
low: 7,
|
|
688
|
+
close: 10,
|
|
689
|
+
}));
|
|
690
|
+
const scene = buildCompositeChartScene(
|
|
691
|
+
[candles],
|
|
692
|
+
[{ id: "main" }],
|
|
693
|
+
{
|
|
694
|
+
width: 81,
|
|
695
|
+
height: 9,
|
|
696
|
+
viewport: {
|
|
697
|
+
start: new Date("2025-01-01T00:00:00.000Z"),
|
|
698
|
+
end: new Date("2025-01-03T00:00:00.000Z"),
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
)!;
|
|
702
|
+
const panel = scene.panels[0]!;
|
|
703
|
+
const bitmap = renderCompositePanelBitmap(panel, {
|
|
704
|
+
pixelWidth: 101,
|
|
705
|
+
pixelHeight: 51,
|
|
706
|
+
colors: {
|
|
707
|
+
background: "#000000",
|
|
708
|
+
grid: "#202020",
|
|
709
|
+
crosshair: "#ffffff",
|
|
710
|
+
text: "#eeeeee",
|
|
711
|
+
textDim: "#999999",
|
|
712
|
+
negative: "#ff0000",
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
const domain = panel.axes.left!;
|
|
716
|
+
const bodyYRatio = (
|
|
717
|
+
projectCompositeValue(8, domain)!
|
|
718
|
+
+ projectCompositeValue(10, domain)!
|
|
719
|
+
) / 2;
|
|
720
|
+
const bodyY = Math.round(bodyYRatio * (bitmap.height - 1));
|
|
721
|
+
const coloredColumns = Array.from({ length: bitmap.width }, (_, x) => x)
|
|
722
|
+
.filter((x) => {
|
|
723
|
+
const offset = (bodyY * bitmap.width + x) * 4;
|
|
724
|
+
return bitmap.pixels[offset]! < 20
|
|
725
|
+
&& bitmap.pixels[offset + 1]! > 220
|
|
726
|
+
&& bitmap.pixels[offset + 2]! > 70;
|
|
727
|
+
});
|
|
728
|
+
const runs: number[][] = [];
|
|
729
|
+
for (const x of coloredColumns) {
|
|
730
|
+
const last = runs.at(-1);
|
|
731
|
+
if (last?.at(-1) === x - 1) last.push(x);
|
|
732
|
+
else runs.push([x]);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
expect(runs).toHaveLength(3);
|
|
736
|
+
expect(runs.map((run) => run.length)).toEqual([13, 13, 13]);
|
|
737
|
+
expect(runs[0]?.[0]).toBe(0);
|
|
738
|
+
expect(runs.at(-1)?.at(-1)).toBe(bitmap.width - 1);
|
|
739
|
+
});
|
|
740
|
+
|
|
681
741
|
test("marks a standalone line observation without extending it through time", () => {
|
|
682
742
|
const scene = buildCompositeChartScene(
|
|
683
743
|
[series("estimate", "line", [42], "left")],
|
|
@@ -431,10 +431,15 @@ export function buildCompositeChartScene(
|
|
|
431
431
|
startTime,
|
|
432
432
|
endTime,
|
|
433
433
|
);
|
|
434
|
-
const
|
|
434
|
+
const scopedSeries = viewport
|
|
435
435
|
? dataSeries.flatMap((entry) => scopeSeriesToViewport(entry, startTime, endTime, timeScale) ?? [])
|
|
436
436
|
: dataSeries;
|
|
437
|
-
|
|
437
|
+
// A range without observations still has a chart: keep the panels, axes and
|
|
438
|
+
// grid and simply draw no points, instead of blanking the whole surface.
|
|
439
|
+
const emptyRange = scopedSeries.length === 0;
|
|
440
|
+
const usableSeries = emptyRange
|
|
441
|
+
? dataSeries.map((entry) => ({ ...entry, points: [] }))
|
|
442
|
+
: scopedSeries;
|
|
438
443
|
const visibleTimes = uniqueTimes.filter((time) => time >= startTime && time <= endTime);
|
|
439
444
|
const marketTimes = timeScale.kind === "market"
|
|
440
445
|
? timeScale.anchors
|
|
@@ -463,8 +468,13 @@ export function buildCompositeChartScene(
|
|
|
463
468
|
const panelSeries = usableSeries.filter((entry) => entry.panelId === panel.id);
|
|
464
469
|
if (panelSeries.length === 0) return [];
|
|
465
470
|
const scale = panel.scale ?? "linear";
|
|
466
|
-
|
|
467
|
-
|
|
471
|
+
// An empty range has no in-view values, so scale its axes to the loaded
|
|
472
|
+
// history rather than the meaningless 0..1 fallback.
|
|
473
|
+
const domainSeries = emptyRange
|
|
474
|
+
? dataSeries.filter((entry) => entry.panelId === panel.id)
|
|
475
|
+
: panelSeries;
|
|
476
|
+
const left = buildAxisDomain("left", domainSeries, scale);
|
|
477
|
+
const right = buildAxisDomain("right", domainSeries, scale);
|
|
468
478
|
const axes: Partial<Record<CompositeAxisSide, CompositeAxisDomain>> = { left, right };
|
|
469
479
|
return [{
|
|
470
480
|
id: panel.id,
|