gloomberb 0.9.4 → 0.9.6
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 +49 -8
- package/package.json +4 -3
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- package/src/app/global-shortcuts.test.tsx +20 -1
- package/src/app/global-shortcuts.ts +2 -0
- package/src/app/pane-runtime/index.tsx +11 -4
- package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
- package/src/app.tsx +41 -17
- package/src/cli/commands/remote.ts +206 -0
- package/src/cli/entry.ts +8 -1
- package/src/cli/index.ts +2 -0
- package/src/cli/native-smoke.ts +5 -0
- package/src/components/chart/core/keyboard.ts +10 -1
- package/src/components/chart/native/bitmap-support.test.ts +27 -0
- package/src/components/chart/native/bitmap-support.ts +55 -0
- package/src/components/chart/native/kitty/index.test.ts +47 -0
- package/src/components/chart/native/surface/visibility.ts +8 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
- package/src/components/chart/static/chart/bitmap.ts +16 -9
- package/src/components/chart/static/chart/surface.test.tsx +59 -4
- package/src/components/chart/static/chart/surface.tsx +4 -0
- package/src/components/chart/stock-chart/index.test.ts +7 -0
- package/src/components/command-bar/list/view.tsx +48 -1
- package/src/components/command-bar/multi-select-picker.tsx +7 -0
- package/src/components/command-bar/routing/navigation-state.ts +10 -1
- package/src/components/command-bar/surface/index.test.tsx +14 -0
- package/src/components/command-bar/theme-picker.tsx +91 -79
- package/src/components/command-bar/workflow/field-row.tsx +30 -5
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/index.ts +7 -0
- package/src/components/layout/floating-pane.tsx +3 -3
- package/src/components/layout/pane/footer/index.tsx +23 -0
- package/src/components/layout/pane/header.tsx +5 -0
- package/src/components/layout/pane/index.tsx +3 -3
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +9 -1
- package/src/components/layout/shell/index.test.tsx +2 -0
- package/src/components/layout/shell/index.tsx +16 -4
- package/src/components/layout/shell/pane/actions.ts +21 -7
- package/src/components/layout/shell/pane/layers.tsx +2 -2
- package/src/components/layout/status-bar.tsx +5 -5
- package/src/components/metric-treemap/index.tsx +317 -0
- package/src/components/metric-treemap/layout.test.ts +77 -0
- package/src/components/metric-treemap/layout.ts +476 -0
- package/src/components/pane-settings-dialog/desktop.tsx +1 -1
- package/src/components/price-performance.tsx +1 -1
- package/src/components/price-sparkline/view.tsx +15 -14
- package/src/components/speedometer-gauge/terminal.tsx +18 -15
- package/src/components/table-view-shared.tsx +0 -10
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/toggle-list.tsx +10 -0
- package/src/components/ui/button.tsx +12 -0
- package/src/components/ui/data-table/index.tsx +51 -0
- package/src/components/ui/data-table/opentui/index.tsx +4 -10
- package/src/components/ui/data-table/types.ts +6 -4
- package/src/components/ui/fields.tsx +18 -0
- package/src/components/ui/list-view.tsx +84 -4
- package/src/components/ui/message-composer.tsx +22 -0
- package/src/components/ui/tabs.tsx +46 -2
- package/src/components/ui/ui.test.tsx +0 -9
- package/src/core/state/app/layout-reducer.ts +7 -1
- package/src/core/state/app/layout.ts +6 -0
- package/src/core/state/app/state.ts +23 -1
- package/src/core/state/app/types.ts +4 -2
- package/src/plugins/builtin/account-management/form-components.tsx +2 -2
- package/src/plugins/builtin/alerts/pane.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +5 -18
- package/src/plugins/builtin/chat/chat.test.tsx +1 -7
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
- package/src/plugins/builtin/chat/sidebar.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +2 -8
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- package/src/plugins/builtin/cloud/plugin.tsx +4 -13
- package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
- package/src/plugins/builtin/debug/index.tsx +3 -4
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/holders/treemap.tsx +28 -297
- package/src/plugins/builtin/holders/types.ts +0 -34
- package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
- package/src/plugins/builtin/market-heatmap/data.ts +296 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
- package/src/plugins/builtin/options/table.ts +5 -5
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
- package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
- package/src/plugins/ibkr/gateway/service/index.ts +6 -9
- package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
- package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
- package/src/remote/app-host.tsx +77 -0
- package/src/remote/client.ts +34 -0
- package/src/remote/command-bar.ts +98 -0
- package/src/remote/controller-utils.ts +111 -0
- package/src/remote/controller.test.ts +365 -0
- package/src/remote/controller.ts +434 -0
- package/src/remote/endpoint.ts +41 -0
- package/src/remote/json-patch.test.ts +31 -0
- package/src/remote/json-patch.ts +89 -0
- package/src/remote/layout-helpers.ts +62 -0
- package/src/remote/resources.ts +186 -0
- package/src/remote/revision.ts +18 -0
- package/src/remote/schema.ts +143 -0
- package/src/remote/semantic-helpers.ts +161 -0
- package/src/remote/semantic-tree.tsx +112 -0
- package/src/remote/server.test.ts +50 -0
- package/src/remote/server.ts +121 -0
- package/src/remote/types.ts +119 -0
- package/src/renderers/electrobun/bun/index.ts +55 -1
- package/src/renderers/electrobun/shared/protocol.ts +11 -1
- package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
- package/src/renderers/electrobun/view/build-assets.ts +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
- package/src/renderers/electrobun/view/host/box.tsx +11 -1
- package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
- package/src/renderers/electrobun/view/host/mouse.ts +2 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/main.tsx +5 -1
- package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
- package/src/renderers/electrobun/view/styles.css +7 -0
- package/src/renderers/opentui/chart-surface.test.tsx +101 -0
- package/src/renderers/opentui/chart-surface.tsx +15 -20
- package/src/renderers/opentui/native-smoke.ts +1 -0
- package/src/renderers/opentui/start.tsx +32 -0
- package/src/sources/yahoo-finance/http.ts +31 -4
- package/src/state/app/context/index.test.tsx +47 -0
- package/src/state/app/context/index.tsx +48 -18
- package/src/ui/host.tsx +1 -0
- package/src/ui/index.tsx +183 -7
- package/src/version.ts +1 -1
- package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildMetricTreemap,
|
|
4
|
+
buildMetricTreemapRects,
|
|
5
|
+
findMetricTreemapNeighbor,
|
|
6
|
+
type MetricTreemapItem,
|
|
7
|
+
type MetricTreemapTile,
|
|
8
|
+
} from "./layout";
|
|
9
|
+
|
|
10
|
+
const ITEMS: Array<MetricTreemapItem<string>> = [
|
|
11
|
+
{ id: "a", label: "A", weight: 3, data: "A" },
|
|
12
|
+
{ id: "b", label: "B", weight: 1, data: "B" },
|
|
13
|
+
{ id: "zero", label: "Zero", weight: 0, data: "Zero" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe("metric treemap layout", () => {
|
|
17
|
+
test("lays out positive weighted items inside integer bounds", () => {
|
|
18
|
+
const tiles = buildMetricTreemap(ITEMS, 12, 6);
|
|
19
|
+
|
|
20
|
+
expect(tiles.map((tile) => tile.item.id)).toEqual(["a", "b"]);
|
|
21
|
+
for (const tile of tiles) {
|
|
22
|
+
expect(tile.x).toBeGreaterThanOrEqual(0);
|
|
23
|
+
expect(tile.y).toBeGreaterThanOrEqual(0);
|
|
24
|
+
expect(tile.width).toBeGreaterThan(0);
|
|
25
|
+
expect(tile.height).toBeGreaterThan(0);
|
|
26
|
+
expect(tile.x + tile.width).toBeLessThanOrEqual(12);
|
|
27
|
+
expect(tile.y + tile.height).toBeLessThanOrEqual(6);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("produces proportional floating rects for native rendering", () => {
|
|
32
|
+
const tiles = buildMetricTreemapRects(ITEMS, 100, 40);
|
|
33
|
+
const first = tiles[0];
|
|
34
|
+
const second = tiles[1];
|
|
35
|
+
|
|
36
|
+
expect(first?.item.id).toBe("a");
|
|
37
|
+
expect(second?.item.id).toBe("b");
|
|
38
|
+
expect((first?.width ?? 0) * (first?.height ?? 0)).toBeGreaterThan((second?.width ?? 0) * (second?.height ?? 0));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("drops low-ranked float tiles before they render as slivers", () => {
|
|
42
|
+
const denseItems = Array.from({ length: 96 }, (_, index): MetricTreemapItem<number> => ({
|
|
43
|
+
id: `item-${index}`,
|
|
44
|
+
label: `${index}`,
|
|
45
|
+
weight: 100 - index,
|
|
46
|
+
data: index,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const tiles = buildMetricTreemapRects(denseItems, 52, 32, 18 / 8);
|
|
50
|
+
|
|
51
|
+
expect(tiles.length).toBeLessThan(denseItems.length);
|
|
52
|
+
expect(tiles.every((tile) => tile.width >= 3 && tile.height >= 1.8)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("finds neighbors by rendered geometry instead of source order", () => {
|
|
56
|
+
const tiles: Array<MetricTreemapTile<string>> = [
|
|
57
|
+
{ item: { id: "a", label: "A", weight: 1, data: "a" }, x: 0, y: 0, width: 5, height: 5 },
|
|
58
|
+
{ item: { id: "below", label: "Below", weight: 1, data: "below" }, x: 0, y: 5, width: 5, height: 5 },
|
|
59
|
+
{ item: { id: "right", label: "Right", weight: 1, data: "right" }, x: 5, y: 0, width: 5, height: 5 },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
expect(findMetricTreemapNeighbor(tiles, "a", "right")?.item.id).toBe("right");
|
|
63
|
+
expect(findMetricTreemapNeighbor(tiles, "a", "down")?.item.id).toBe("below");
|
|
64
|
+
expect(findMetricTreemapNeighbor(tiles, "right", "left")?.item.id).toBe("a");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("does not choose an overlapping lower tile for right navigation", () => {
|
|
68
|
+
const tiles: Array<MetricTreemapTile<string>> = [
|
|
69
|
+
{ item: { id: "current", label: "Current", weight: 1, data: "current" }, x: 33, y: 0, width: 26, height: 10 },
|
|
70
|
+
{ item: { id: "lower", label: "Lower", weight: 1, data: "lower" }, x: 49, y: 10, width: 12, height: 6 },
|
|
71
|
+
{ item: { id: "right", label: "Right", weight: 1, data: "right" }, x: 59, y: 0, width: 24, height: 10 },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
expect(findMetricTreemapNeighbor(tiles, "current", "right")?.item.id).toBe("right");
|
|
75
|
+
expect(findMetricTreemapNeighbor(tiles, "current", "down")?.item.id).toBe("lower");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
export interface MetricTreemapItem<T = unknown> {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
weight: number | null | undefined;
|
|
5
|
+
colorValue?: number | null;
|
|
6
|
+
primaryText?: string | null;
|
|
7
|
+
secondaryText?: string | null;
|
|
8
|
+
tertiaryText?: string | null;
|
|
9
|
+
data: T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MetricTreemapTile<T = unknown> {
|
|
13
|
+
item: MetricTreemapItem<T>;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FloatMetricTreemapTile<T = unknown> {
|
|
21
|
+
item: MetricTreemapItem<T>;
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type MetricTreemapDirection = "left" | "right" | "up" | "down";
|
|
29
|
+
export type MetricTreemapLayoutMode = "integer" | "float";
|
|
30
|
+
|
|
31
|
+
export interface MetricTreemapLayoutOptions {
|
|
32
|
+
maxItems?: number;
|
|
33
|
+
minTileWidth?: number;
|
|
34
|
+
minTileHeight?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WeightedTreemapItem<T> {
|
|
38
|
+
item: MetricTreemapItem<T>;
|
|
39
|
+
weight: number;
|
|
40
|
+
area: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface TreemapGroup<T> {
|
|
44
|
+
items: WeightedTreemapItem<T>[];
|
|
45
|
+
weight: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface FloatRect {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MAX_TREEMAP_ITEMS = 160;
|
|
56
|
+
const DEFAULT_MIN_TILE_WIDTH = 3;
|
|
57
|
+
const DEFAULT_MIN_TILE_HEIGHT = 1.8;
|
|
58
|
+
|
|
59
|
+
function itemWeight(item: MetricTreemapItem): number {
|
|
60
|
+
return Math.max(item.weight ?? 0, 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function worstAspectRatio<T>(items: WeightedTreemapItem<T>[], sideLength: number): number {
|
|
64
|
+
if (items.length === 0 || sideLength <= 0) return Number.POSITIVE_INFINITY;
|
|
65
|
+
const areaSum = items.reduce((sum, item) => sum + item.area, 0);
|
|
66
|
+
const minArea = Math.min(...items.map((item) => item.area));
|
|
67
|
+
const maxArea = Math.max(...items.map((item) => item.area));
|
|
68
|
+
if (areaSum <= 0 || minArea <= 0) return Number.POSITIVE_INFINITY;
|
|
69
|
+
|
|
70
|
+
const sideSquared = sideLength * sideLength;
|
|
71
|
+
return Math.max(
|
|
72
|
+
(sideSquared * maxArea) / (areaSum * areaSum),
|
|
73
|
+
(areaSum * areaSum) / (sideSquared * minArea),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function layoutFloatGroup<T>(items: WeightedTreemapItem<T>[], rect: FloatRect): FloatRect {
|
|
78
|
+
const areaSum = items.reduce((sum, item) => sum + item.area, 0);
|
|
79
|
+
if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) return rect;
|
|
80
|
+
|
|
81
|
+
if (rect.width >= rect.height) {
|
|
82
|
+
const columnWidth = Math.min(rect.width, areaSum / rect.height);
|
|
83
|
+
return {
|
|
84
|
+
x: rect.x + columnWidth,
|
|
85
|
+
y: rect.y,
|
|
86
|
+
width: Math.max(0, rect.width - columnWidth),
|
|
87
|
+
height: rect.height,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rowHeight = Math.min(rect.height, areaSum / rect.width);
|
|
92
|
+
return {
|
|
93
|
+
x: rect.x,
|
|
94
|
+
y: rect.y + rowHeight,
|
|
95
|
+
width: rect.width,
|
|
96
|
+
height: Math.max(0, rect.height - rowHeight),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildSquarifiedGroups<T>(items: WeightedTreemapItem<T>[], width: number, height: number): TreemapGroup<T>[] {
|
|
101
|
+
const groups: TreemapGroup<T>[] = [];
|
|
102
|
+
let rect: FloatRect = { x: 0, y: 0, width, height };
|
|
103
|
+
let index = 0;
|
|
104
|
+
|
|
105
|
+
while (index < items.length && rect.width > 0 && rect.height > 0) {
|
|
106
|
+
const group: WeightedTreemapItem<T>[] = [];
|
|
107
|
+
let currentWorst = Number.POSITIVE_INFINITY;
|
|
108
|
+
const sideLength = Math.min(rect.width, rect.height);
|
|
109
|
+
|
|
110
|
+
while (index < items.length) {
|
|
111
|
+
const candidate = items[index]!;
|
|
112
|
+
const nextGroup = [...group, candidate];
|
|
113
|
+
const nextWorst = worstAspectRatio(nextGroup, sideLength);
|
|
114
|
+
if (group.length > 0 && nextWorst > currentWorst) break;
|
|
115
|
+
group.push(candidate);
|
|
116
|
+
currentWorst = nextWorst;
|
|
117
|
+
index += 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
groups.push({
|
|
121
|
+
items: group,
|
|
122
|
+
weight: group.reduce((sum, item) => sum + item.weight, 0),
|
|
123
|
+
});
|
|
124
|
+
rect = layoutFloatGroup(group, rect);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return groups;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeWeightedItems<T>(
|
|
131
|
+
items: MetricTreemapItem<T>[],
|
|
132
|
+
width: number,
|
|
133
|
+
height: number,
|
|
134
|
+
cellAspect: number,
|
|
135
|
+
maxItems: number,
|
|
136
|
+
): WeightedTreemapItem<T>[] {
|
|
137
|
+
const weightedItems = items
|
|
138
|
+
.map((item) => ({ item, weight: itemWeight(item) }))
|
|
139
|
+
.filter((item) => item.weight > 0)
|
|
140
|
+
.slice(0, maxItems);
|
|
141
|
+
const totalWeight = weightedItems.reduce((sum, item) => sum + item.weight, 0);
|
|
142
|
+
if (totalWeight <= 0) return [];
|
|
143
|
+
|
|
144
|
+
const normalizedHeight = height * cellAspect;
|
|
145
|
+
const totalArea = width * normalizedHeight;
|
|
146
|
+
return weightedItems.map((item) => ({
|
|
147
|
+
...item,
|
|
148
|
+
area: item.weight / totalWeight * totalArea,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function maxItemsForLayout(width: number, height: number, options?: MetricTreemapLayoutOptions): number {
|
|
153
|
+
const requested = options?.maxItems ?? width * height;
|
|
154
|
+
return Math.max(1, Math.min(MAX_TREEMAP_ITEMS, Math.floor(requested)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isTooSmall(
|
|
158
|
+
tile: { width: number; height: number },
|
|
159
|
+
options: MetricTreemapLayoutOptions | undefined,
|
|
160
|
+
defaultMinTileWidth: number,
|
|
161
|
+
defaultMinTileHeight: number,
|
|
162
|
+
): boolean {
|
|
163
|
+
const minTileWidth = options?.minTileWidth ?? defaultMinTileWidth;
|
|
164
|
+
const minTileHeight = options?.minTileHeight ?? defaultMinTileHeight;
|
|
165
|
+
return tile.width < minTileWidth || tile.height < minTileHeight;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function allocateLengths(totalLength: number, weights: number[]): number[] {
|
|
169
|
+
if (weights.length === 0 || totalLength <= 0) return [];
|
|
170
|
+
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
|
|
171
|
+
let remainingLength = totalLength;
|
|
172
|
+
let remainingWeight = totalWeight;
|
|
173
|
+
|
|
174
|
+
return weights.map((weight, index) => {
|
|
175
|
+
const remainingItems = weights.length - index;
|
|
176
|
+
if (remainingItems === 1) return remainingLength;
|
|
177
|
+
const ideal = remainingWeight > 0 ? Math.round(remainingLength * weight / remainingWeight) : 1;
|
|
178
|
+
const length = Math.max(1, Math.min(remainingLength - (remainingItems - 1), ideal));
|
|
179
|
+
remainingLength -= length;
|
|
180
|
+
remainingWeight -= weight;
|
|
181
|
+
return length;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildMetricTreemapRectsForLimit<T>(
|
|
186
|
+
items: MetricTreemapItem<T>[],
|
|
187
|
+
width: number,
|
|
188
|
+
height: number,
|
|
189
|
+
cellAspect = 1,
|
|
190
|
+
maxItems = maxItemsForLayout(width, height),
|
|
191
|
+
): FloatMetricTreemapTile<T>[] {
|
|
192
|
+
if (width <= 0 || height <= 0) return [];
|
|
193
|
+
const normalizedCellAspect = Math.max(0.25, cellAspect);
|
|
194
|
+
const weightedItems = normalizeWeightedItems(items, width, height, normalizedCellAspect, maxItems);
|
|
195
|
+
if (weightedItems.length === 0) return [];
|
|
196
|
+
|
|
197
|
+
const normalizedHeight = height * normalizedCellAspect;
|
|
198
|
+
const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
|
|
199
|
+
const tiles: FloatMetricTreemapTile<T>[] = [];
|
|
200
|
+
let rect: FloatRect = { x: 0, y: 0, width, height: normalizedHeight };
|
|
201
|
+
|
|
202
|
+
for (const group of groups) {
|
|
203
|
+
const areaSum = group.items.reduce((sum, item) => sum + item.area, 0);
|
|
204
|
+
if (areaSum <= 0 || rect.width <= 0 || rect.height <= 0) break;
|
|
205
|
+
|
|
206
|
+
if (rect.width >= rect.height) {
|
|
207
|
+
const columnWidth = Math.min(rect.width, areaSum / rect.height);
|
|
208
|
+
let itemY = rect.y;
|
|
209
|
+
for (const item of group.items) {
|
|
210
|
+
const itemHeight = Math.min(rect.y + rect.height - itemY, item.area / columnWidth);
|
|
211
|
+
tiles.push({
|
|
212
|
+
item: item.item,
|
|
213
|
+
x: rect.x,
|
|
214
|
+
y: itemY / normalizedCellAspect,
|
|
215
|
+
width: columnWidth,
|
|
216
|
+
height: itemHeight / normalizedCellAspect,
|
|
217
|
+
});
|
|
218
|
+
itemY += itemHeight;
|
|
219
|
+
}
|
|
220
|
+
rect = {
|
|
221
|
+
x: rect.x + columnWidth,
|
|
222
|
+
y: rect.y,
|
|
223
|
+
width: Math.max(0, rect.width - columnWidth),
|
|
224
|
+
height: rect.height,
|
|
225
|
+
};
|
|
226
|
+
} else {
|
|
227
|
+
const rowHeight = Math.min(rect.height, areaSum / rect.width);
|
|
228
|
+
let itemX = rect.x;
|
|
229
|
+
for (const item of group.items) {
|
|
230
|
+
const itemWidth = Math.min(rect.x + rect.width - itemX, item.area / rowHeight);
|
|
231
|
+
tiles.push({
|
|
232
|
+
item: item.item,
|
|
233
|
+
x: itemX,
|
|
234
|
+
y: rect.y / normalizedCellAspect,
|
|
235
|
+
width: itemWidth,
|
|
236
|
+
height: rowHeight / normalizedCellAspect,
|
|
237
|
+
});
|
|
238
|
+
itemX += itemWidth;
|
|
239
|
+
}
|
|
240
|
+
rect = {
|
|
241
|
+
x: rect.x,
|
|
242
|
+
y: rect.y + rowHeight,
|
|
243
|
+
width: rect.width,
|
|
244
|
+
height: Math.max(0, rect.height - rowHeight),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function buildMetricTreemapRects<T>(
|
|
253
|
+
items: MetricTreemapItem<T>[],
|
|
254
|
+
width: number,
|
|
255
|
+
height: number,
|
|
256
|
+
cellAspect = 1,
|
|
257
|
+
options?: MetricTreemapLayoutOptions,
|
|
258
|
+
): FloatMetricTreemapTile<T>[] {
|
|
259
|
+
let limit = maxItemsForLayout(width, height, options);
|
|
260
|
+
let lastTiles: FloatMetricTreemapTile<T>[] = [];
|
|
261
|
+
|
|
262
|
+
while (limit > 0) {
|
|
263
|
+
const tiles = buildMetricTreemapRectsForLimit(items, width, height, cellAspect, limit);
|
|
264
|
+
lastTiles = tiles;
|
|
265
|
+
const firstSmallIndex = tiles.findIndex((tile) => isTooSmall(tile, options, DEFAULT_MIN_TILE_WIDTH, DEFAULT_MIN_TILE_HEIGHT));
|
|
266
|
+
if (firstSmallIndex < 0) return tiles;
|
|
267
|
+
limit = Math.min(limit - 1, firstSmallIndex);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return lastTiles.filter((tile) => !isTooSmall(tile, options, DEFAULT_MIN_TILE_WIDTH, DEFAULT_MIN_TILE_HEIGHT));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildMetricTreemapForLimit<T>(
|
|
274
|
+
items: MetricTreemapItem<T>[],
|
|
275
|
+
width: number,
|
|
276
|
+
height: number,
|
|
277
|
+
cellAspect = 1,
|
|
278
|
+
maxItems = maxItemsForLayout(width, height),
|
|
279
|
+
): MetricTreemapTile<T>[] {
|
|
280
|
+
if (width <= 0 || height <= 0) return [];
|
|
281
|
+
const normalizedCellAspect = Math.max(0.25, cellAspect);
|
|
282
|
+
const weightedItems = normalizeWeightedItems(items, width, height, normalizedCellAspect, maxItems);
|
|
283
|
+
if (weightedItems.length === 0) return [];
|
|
284
|
+
|
|
285
|
+
const normalizedHeight = height * normalizedCellAspect;
|
|
286
|
+
const groups = buildSquarifiedGroups(weightedItems, width, normalizedHeight);
|
|
287
|
+
const tiles: MetricTreemapTile<T>[] = [];
|
|
288
|
+
|
|
289
|
+
let x = 0;
|
|
290
|
+
let y = 0;
|
|
291
|
+
let remainingWidth = width;
|
|
292
|
+
let remainingHeight = height;
|
|
293
|
+
let remainingWeight = groups.reduce((sum, group) => sum + group.weight, 0);
|
|
294
|
+
|
|
295
|
+
groups.forEach((group, groupIndex) => {
|
|
296
|
+
if (remainingWidth <= 0 || remainingHeight <= 0) return;
|
|
297
|
+
const isLastGroup = groupIndex === groups.length - 1;
|
|
298
|
+
const remainingNormalizedHeight = remainingHeight * normalizedCellAspect;
|
|
299
|
+
|
|
300
|
+
if (remainingWidth >= remainingNormalizedHeight) {
|
|
301
|
+
const columnWidth = isLastGroup
|
|
302
|
+
? remainingWidth
|
|
303
|
+
: Math.max(1, Math.min(remainingWidth - 1, Math.round(remainingWidth * group.weight / remainingWeight)));
|
|
304
|
+
const heights = allocateLengths(remainingHeight, group.items.map((item) => item.weight));
|
|
305
|
+
let tileY = y;
|
|
306
|
+
group.items.forEach((item, itemIndex) => {
|
|
307
|
+
const tileHeight = heights[itemIndex] ?? 0;
|
|
308
|
+
if (tileHeight > 0) {
|
|
309
|
+
tiles.push({ item: item.item, x, y: tileY, width: columnWidth, height: tileHeight });
|
|
310
|
+
}
|
|
311
|
+
tileY += tileHeight;
|
|
312
|
+
});
|
|
313
|
+
x += columnWidth;
|
|
314
|
+
remainingWidth -= columnWidth;
|
|
315
|
+
} else {
|
|
316
|
+
const rowHeight = isLastGroup
|
|
317
|
+
? remainingHeight
|
|
318
|
+
: Math.max(1, Math.min(remainingHeight - 1, Math.round(remainingHeight * group.weight / remainingWeight)));
|
|
319
|
+
const widths = allocateLengths(remainingWidth, group.items.map((item) => item.weight));
|
|
320
|
+
let tileX = x;
|
|
321
|
+
group.items.forEach((item, itemIndex) => {
|
|
322
|
+
const tileWidth = widths[itemIndex] ?? 0;
|
|
323
|
+
if (tileWidth > 0) {
|
|
324
|
+
tiles.push({ item: item.item, x: tileX, y, width: tileWidth, height: rowHeight });
|
|
325
|
+
}
|
|
326
|
+
tileX += tileWidth;
|
|
327
|
+
});
|
|
328
|
+
y += rowHeight;
|
|
329
|
+
remainingHeight -= rowHeight;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
remainingWeight -= group.weight;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return tiles.filter((tile) => tile.width > 0 && tile.height > 0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function buildMetricTreemap<T>(
|
|
339
|
+
items: MetricTreemapItem<T>[],
|
|
340
|
+
width: number,
|
|
341
|
+
height: number,
|
|
342
|
+
cellAspect = 1,
|
|
343
|
+
options?: MetricTreemapLayoutOptions,
|
|
344
|
+
): MetricTreemapTile<T>[] {
|
|
345
|
+
let limit = maxItemsForLayout(width, height, options);
|
|
346
|
+
let lastTiles: MetricTreemapTile<T>[] = [];
|
|
347
|
+
|
|
348
|
+
while (limit > 0) {
|
|
349
|
+
const tiles = buildMetricTreemapForLimit(items, width, height, cellAspect, limit);
|
|
350
|
+
lastTiles = tiles;
|
|
351
|
+
const firstSmallIndex = tiles.findIndex((tile) => isTooSmall(tile, options, 2, 1));
|
|
352
|
+
if (firstSmallIndex < 0) return tiles;
|
|
353
|
+
limit = Math.min(limit - 1, firstSmallIndex);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return lastTiles.filter((tile) => !isTooSmall(tile, options, 2, 1));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function center(rect: { x: number; y: number; width: number; height: number }) {
|
|
360
|
+
return {
|
|
361
|
+
x: rect.x + rect.width / 2,
|
|
362
|
+
y: rect.y + rect.height / 2,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function rangeGap(startA: number, endA: number, startB: number, endB: number): number {
|
|
367
|
+
if (endA < startB) return startB - endA;
|
|
368
|
+
if (endB < startA) return startA - endB;
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function primaryGap(
|
|
373
|
+
current: { x: number; y: number; width: number; height: number },
|
|
374
|
+
candidate: { x: number; y: number; width: number; height: number },
|
|
375
|
+
direction: MetricTreemapDirection,
|
|
376
|
+
): number {
|
|
377
|
+
switch (direction) {
|
|
378
|
+
case "left":
|
|
379
|
+
return Math.max(0, current.x - (candidate.x + candidate.width));
|
|
380
|
+
case "right":
|
|
381
|
+
return Math.max(0, candidate.x - (current.x + current.width));
|
|
382
|
+
case "up":
|
|
383
|
+
return Math.max(0, current.y - (candidate.y + candidate.height));
|
|
384
|
+
case "down":
|
|
385
|
+
return Math.max(0, candidate.y - (current.y + current.height));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function perpendicularGap(
|
|
390
|
+
current: { x: number; y: number; width: number; height: number },
|
|
391
|
+
candidate: { x: number; y: number; width: number; height: number },
|
|
392
|
+
direction: MetricTreemapDirection,
|
|
393
|
+
): number {
|
|
394
|
+
if (direction === "left" || direction === "right") {
|
|
395
|
+
return rangeGap(current.y, current.y + current.height, candidate.y, candidate.y + candidate.height);
|
|
396
|
+
}
|
|
397
|
+
return rangeGap(current.x, current.x + current.width, candidate.x, candidate.x + candidate.width);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isEdgeCandidate(
|
|
401
|
+
current: { x: number; y: number; width: number; height: number },
|
|
402
|
+
candidate: { x: number; y: number; width: number; height: number },
|
|
403
|
+
direction: MetricTreemapDirection,
|
|
404
|
+
): boolean {
|
|
405
|
+
const epsilon = 0.0001;
|
|
406
|
+
switch (direction) {
|
|
407
|
+
case "left":
|
|
408
|
+
return candidate.x + candidate.width <= current.x + epsilon;
|
|
409
|
+
case "right":
|
|
410
|
+
return candidate.x >= current.x + current.width - epsilon;
|
|
411
|
+
case "up":
|
|
412
|
+
return candidate.y + candidate.height <= current.y + epsilon;
|
|
413
|
+
case "down":
|
|
414
|
+
return candidate.y >= current.y + current.height - epsilon;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function isCenterCandidate(
|
|
419
|
+
currentCenter: { x: number; y: number },
|
|
420
|
+
candidateCenter: { x: number; y: number },
|
|
421
|
+
direction: MetricTreemapDirection,
|
|
422
|
+
): boolean {
|
|
423
|
+
switch (direction) {
|
|
424
|
+
case "left":
|
|
425
|
+
return candidateCenter.x < currentCenter.x;
|
|
426
|
+
case "right":
|
|
427
|
+
return candidateCenter.x > currentCenter.x;
|
|
428
|
+
case "up":
|
|
429
|
+
return candidateCenter.y < currentCenter.y;
|
|
430
|
+
case "down":
|
|
431
|
+
return candidateCenter.y > currentCenter.y;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function findMetricTreemapNeighbor<T>(
|
|
436
|
+
tiles: Array<MetricTreemapTile<T> | FloatMetricTreemapTile<T>>,
|
|
437
|
+
selectedId: string | null,
|
|
438
|
+
direction: MetricTreemapDirection,
|
|
439
|
+
): MetricTreemapTile<T> | FloatMetricTreemapTile<T> | null {
|
|
440
|
+
if (tiles.length === 0) return null;
|
|
441
|
+
const current = tiles.find((tile) => tile.item.id === selectedId) ?? tiles[0]!;
|
|
442
|
+
const currentCenter = center(current);
|
|
443
|
+
let best: { tile: MetricTreemapTile<T> | FloatMetricTreemapTile<T>; score: number } | null = null;
|
|
444
|
+
const hasEdgeCandidate = tiles.some((tile) => tile.item.id !== current.item.id && isEdgeCandidate(current, tile, direction));
|
|
445
|
+
|
|
446
|
+
for (const tile of tiles) {
|
|
447
|
+
if (tile.item.id === current.item.id) continue;
|
|
448
|
+
const candidateCenter = center(tile);
|
|
449
|
+
const isCandidate = hasEdgeCandidate
|
|
450
|
+
? isEdgeCandidate(current, tile, direction)
|
|
451
|
+
: isCenterCandidate(currentCenter, candidateCenter, direction);
|
|
452
|
+
if (!isCandidate) continue;
|
|
453
|
+
|
|
454
|
+
const centerDistance = direction === "left" || direction === "right"
|
|
455
|
+
? Math.abs(candidateCenter.x - currentCenter.x)
|
|
456
|
+
: Math.abs(candidateCenter.y - currentCenter.y);
|
|
457
|
+
const score = primaryGap(current, tile, direction) * 1000
|
|
458
|
+
+ perpendicularGap(current, tile, direction) * 100
|
|
459
|
+
+ centerDistance;
|
|
460
|
+
if (!best || score < best.score) best = { tile, score };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return best?.tile ?? null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function buildMetricTreemapNavigationTiles<T>(
|
|
467
|
+
items: MetricTreemapItem<T>[],
|
|
468
|
+
width: number,
|
|
469
|
+
height: number,
|
|
470
|
+
cellAspect: number,
|
|
471
|
+
mode: MetricTreemapLayoutMode,
|
|
472
|
+
): Array<MetricTreemapTile<T> | FloatMetricTreemapTile<T>> {
|
|
473
|
+
return mode === "float"
|
|
474
|
+
? buildMetricTreemapRects(items, width, height, cellAspect)
|
|
475
|
+
: buildMetricTreemap(items, width, height, cellAspect);
|
|
476
|
+
}
|
|
@@ -183,7 +183,7 @@ function DesktopSettingsRow({
|
|
|
183
183
|
flexDirection="column"
|
|
184
184
|
minHeight={field.description ? undefined : 2}
|
|
185
185
|
backgroundColor={hovered || selected ? desktopHoverSurface() : "transparent"}
|
|
186
|
-
|
|
186
|
+
onMouseOver={onHover}
|
|
187
187
|
onMouseDown={field.type === "select" ? undefined : (event: any) => {
|
|
188
188
|
event.stopPropagation?.();
|
|
189
189
|
event.preventDefault?.();
|
|
@@ -157,7 +157,7 @@ export function PriceReturnTable({
|
|
|
157
157
|
height={1}
|
|
158
158
|
width={width}
|
|
159
159
|
backgroundColor={row.selected ? blendHex(colors.panel, colors.borderFocused, 0.18) : colors.panel}
|
|
160
|
-
|
|
160
|
+
onMouseOver={() => onSelectSymbol(row.symbol)}
|
|
161
161
|
onMouseDown={(event: any) => {
|
|
162
162
|
onFocusInteraction?.(event);
|
|
163
163
|
onSelectSymbol(row.symbol);
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { colors } from "../../theme/colors";
|
|
11
11
|
import { useThemeColors } from "../../theme/theme-context";
|
|
12
12
|
import type { PricePoint } from "../../types/financials";
|
|
13
|
-
import {
|
|
13
|
+
import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../chart/native/bitmap-support";
|
|
14
14
|
import { renderSparklineBitmap } from "./bitmap";
|
|
15
15
|
import {
|
|
16
16
|
buildSamples,
|
|
@@ -90,22 +90,21 @@ function TerminalPriceSparkline({
|
|
|
90
90
|
const { nativeCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
|
|
91
91
|
const nativeRenderer = useNativeRenderer();
|
|
92
92
|
const rendererResolution = nativeRenderer.resolution;
|
|
93
|
+
const rendererCapabilities = nativeRenderer.capabilities;
|
|
93
94
|
const rendererTerminalWidth = nativeRenderer.terminalWidth;
|
|
94
95
|
const rendererTerminalHeight = nativeRenderer.terminalHeight;
|
|
95
96
|
const bitmap = useMemo(() => {
|
|
96
|
-
if (
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
pixelHeight: Math.max(1, Math.round(height * cellHeightPx * scale)),
|
|
108
|
-
};
|
|
97
|
+
if (values.length < 2 || !shouldRenderNativeBitmap(nativeRenderer, nativeCharts === true)) return null;
|
|
98
|
+
const bitmapSize = resolveNativeBitmapSize({
|
|
99
|
+
width,
|
|
100
|
+
height,
|
|
101
|
+
resolution: rendererResolution,
|
|
102
|
+
terminalWidth: rendererTerminalWidth,
|
|
103
|
+
terminalHeight: rendererTerminalHeight,
|
|
104
|
+
cellWidthPx,
|
|
105
|
+
cellHeightPx,
|
|
106
|
+
pixelRatio,
|
|
107
|
+
});
|
|
109
108
|
return renderSparklineBitmap(values, bitmapSize.pixelWidth, bitmapSize.pixelHeight, color, { area, compact: height <= 1 });
|
|
110
109
|
}, [
|
|
111
110
|
area,
|
|
@@ -114,7 +113,9 @@ function TerminalPriceSparkline({
|
|
|
114
113
|
color,
|
|
115
114
|
height,
|
|
116
115
|
nativeCharts,
|
|
116
|
+
nativeRenderer,
|
|
117
117
|
pixelRatio,
|
|
118
|
+
rendererCapabilities,
|
|
118
119
|
rendererResolution,
|
|
119
120
|
rendererTerminalHeight,
|
|
120
121
|
rendererTerminalWidth,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { Box, ChartSurface, Span, Text, TextAttributes, useNativeRenderer, useUiCapabilities } from "../../ui";
|
|
3
|
-
import {
|
|
3
|
+
import type { NativeChartBitmap } from "../chart/native/chart-rasterizer";
|
|
4
|
+
import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../chart/native/bitmap-support";
|
|
4
5
|
import { drawCircle, drawLine, parseHex } from "../chart/native/raster/primitives";
|
|
5
6
|
import { colors } from "../../theme/colors";
|
|
6
7
|
import {
|
|
@@ -203,29 +204,29 @@ export function TerminalSpeedometerGauge({
|
|
|
203
204
|
const labelRow = useMemo(() => renderLabelRow(gaugeWidth, min, max, segments), [gaugeWidth, max, min, segments]);
|
|
204
205
|
const tickRow = useMemo(() => renderTickRow(gaugeWidth, min, max), [gaugeWidth, max, min]);
|
|
205
206
|
const arcRows = useMemo(() => renderArcRows(value, min, max, gaugeWidth, segments), [gaugeWidth, max, min, segments, value]);
|
|
207
|
+
const rendererCapabilities = nativeRenderer.capabilities;
|
|
206
208
|
const rendererResolution = nativeRenderer.resolution;
|
|
207
209
|
const rendererTerminalWidth = nativeRenderer.terminalWidth;
|
|
208
210
|
const rendererTerminalHeight = nativeRenderer.terminalHeight;
|
|
209
211
|
const bitmap = useMemo<NativeChartBitmap | null>(() => {
|
|
210
|
-
if (!nativeCharts) return null;
|
|
211
|
-
const bitmapSize =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const pixelHeight = bitmapSize?.pixelHeight ?? Math.max(1, Math.round(arcRows.length * cellHeightPx * scale));
|
|
212
|
+
if (!shouldRenderNativeBitmap(nativeRenderer, nativeCharts === true)) return null;
|
|
213
|
+
const bitmapSize = resolveNativeBitmapSize({
|
|
214
|
+
width: gaugeWidth,
|
|
215
|
+
height: arcRows.length,
|
|
216
|
+
resolution: rendererResolution,
|
|
217
|
+
terminalWidth: rendererTerminalWidth,
|
|
218
|
+
terminalHeight: rendererTerminalHeight,
|
|
219
|
+
cellWidthPx,
|
|
220
|
+
cellHeightPx,
|
|
221
|
+
pixelRatio,
|
|
222
|
+
});
|
|
222
223
|
return renderGaugeBitmap(
|
|
223
224
|
value,
|
|
224
225
|
min,
|
|
225
226
|
max,
|
|
226
227
|
segments,
|
|
227
|
-
pixelWidth,
|
|
228
|
-
pixelHeight,
|
|
228
|
+
bitmapSize.pixelWidth,
|
|
229
|
+
bitmapSize.pixelHeight,
|
|
229
230
|
);
|
|
230
231
|
}, [
|
|
231
232
|
arcRows.length,
|
|
@@ -235,7 +236,9 @@ export function TerminalSpeedometerGauge({
|
|
|
235
236
|
max,
|
|
236
237
|
min,
|
|
237
238
|
nativeCharts,
|
|
239
|
+
nativeRenderer,
|
|
238
240
|
pixelRatio,
|
|
241
|
+
rendererCapabilities,
|
|
239
242
|
rendererResolution,
|
|
240
243
|
rendererTerminalHeight,
|
|
241
244
|
rendererTerminalWidth,
|