gloomberb 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/data.ts +18 -3
- package/src/api-client/index.ts +3 -0
- package/src/api-client/members.ts +26 -0
- package/src/api-client/perps.ts +4 -0
- package/src/api-client/types.ts +7 -1
- package/src/app/global-shortcuts.ts +14 -6
- package/src/app/keybindings/actions.ts +1 -1
- package/src/app/runtime/update.ts +38 -2
- package/src/app.tsx +10 -6
- package/src/cli/pane-functions/function-help.ts +9 -2
- package/src/components/command-bar/commands/collection.ts +5 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -2
- package/src/components/command-bar/routes/root/plugin-install.ts +2 -2
- package/src/components/command-bar/routes/root/provider-search.ts +19 -5
- package/src/components/command-bar/routes/root/runtime.ts +14 -5
- package/src/components/command-bar/routes/ticker-search/results.ts +31 -5
- package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
- package/src/components/command-bar/routing/actions.ts +2 -0
- package/src/components/command-bar/selection-runtime.ts +38 -0
- package/src/components/command-bar/surface/index.tsx +7 -2
- package/src/components/index.ts +2 -2
- package/src/components/layout/floating-pane.tsx +6 -0
- package/src/components/layout/header.tsx +24 -7
- package/src/components/layout/missing-pane.tsx +2 -2
- package/src/components/layout/pane/header.tsx +63 -30
- package/src/components/layout/pane/index.tsx +6 -0
- package/src/components/layout/pane/title.ts +31 -7
- package/src/components/layout/shell/drag/index.ts +6 -5
- package/src/components/layout/shell/drag/runtime.ts +3 -0
- package/src/components/layout/shell/index.tsx +6 -0
- package/src/components/layout/shell/pane/layers.tsx +15 -7
- package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -3
- package/src/components/layout/shell/ticker-link-menu.ts +23 -9
- package/src/components/metric-treemap/heat-surface.tsx +718 -0
- package/src/components/metric-treemap/heat-tiles.ts +210 -0
- package/src/components/metric-treemap/index.tsx +2 -0
- package/src/components/metric-treemap/layout.ts +6 -0
- package/src/components/metric-treemap/scene.ts +432 -0
- package/src/components/pane-settings-dialog/field-dialogs.tsx +1 -0
- package/src/components/toggle-list.tsx +38 -12
- package/src/components/ui/country-flag-art.tsx +449 -0
- package/src/components/ui/country-flag.tsx +41 -0
- package/src/components/ui/data-table/types.ts +13 -0
- package/src/components/ui/icon.tsx +2 -0
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/multi-select/dialog.tsx +108 -22
- package/src/components/ui/range-track.tsx +46 -0
- package/src/components/ui/tabs.tsx +4 -0
- package/src/core/state/app/types.ts +10 -1
- package/src/i18n/es.ts +5 -3
- package/src/i18n/ja.ts +5 -3
- package/src/i18n/ko.ts +5 -3
- package/src/i18n/zh-cn.ts +5 -3
- package/src/i18n/zh-tw.ts +5 -3
- package/src/layout/ticker-navigation.ts +28 -2
- package/src/market-data/quotes/contributions.ts +6 -0
- package/src/market-data/quotes/freshness.ts +36 -0
- package/src/market-data/quotes/resolution.ts +22 -8
- package/src/market-data/quotes/time.ts +16 -0
- package/src/plugins/builtin/analytics/risk-client.ts +185 -59
- package/src/plugins/builtin/analytics/risk-headless.ts +55 -49
- package/src/plugins/builtin/analytics/risk-math.ts +33 -13
- package/src/plugins/builtin/analytics/risk-model.ts +250 -60
- package/src/plugins/builtin/analytics/risk-pane.tsx +61 -30
- package/src/plugins/builtin/analytics/risk-test-data.ts +182 -0
- package/src/plugins/builtin/buildout/cells.tsx +15 -12
- package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
- package/src/plugins/builtin/buildout/detail/index.tsx +0 -1
- package/src/plugins/builtin/buildout/detail/intel.tsx +6 -6
- package/src/plugins/builtin/buildout/detail/site.tsx +4 -7
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -19
- package/src/plugins/builtin/catalysts/index.tsx +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -1
- package/src/plugins/builtin/cds/index.tsx +1 -0
- package/src/plugins/builtin/debt-maturities/wall-bar.tsx +14 -41
- package/src/plugins/builtin/dividend-yield/client.ts +4 -2
- package/src/plugins/builtin/earnings/client.ts +2 -2
- package/src/plugins/builtin/earnings-calls/index.tsx +1 -0
- package/src/plugins/builtin/fx-matrix/direction.ts +81 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +71 -33
- package/src/plugins/builtin/fx-matrix/live-legs.ts +17 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +68 -14
- package/src/plugins/builtin/fx-matrix/settings.ts +62 -0
- package/src/plugins/builtin/jobs/index.tsx +1 -0
- package/src/plugins/builtin/jobs/share-bars.tsx +5 -28
- package/src/plugins/builtin/market-heatmap/data.ts +28 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +250 -79
- package/src/plugins/builtin/market-heatmap/live.ts +88 -0
- package/src/plugins/builtin/market-heatmap/portfolio.ts +27 -7
- package/src/plugins/builtin/market-movers/client.ts +3 -2
- package/src/plugins/builtin/market-movers/headless.ts +8 -1
- package/src/plugins/builtin/market-movers/screener.ts +18 -5
- package/src/plugins/builtin/members/client.ts +36 -0
- package/src/plugins/builtin/members/headless.ts +33 -0
- package/src/plugins/builtin/members/index.tsx +20 -0
- package/src/plugins/builtin/members/model.ts +60 -0
- package/src/plugins/builtin/members/pane.tsx +168 -0
- package/src/plugins/builtin/mna/index.tsx +2 -1
- package/src/plugins/builtin/mna/pane.tsx +5 -2
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/pe-band/client.ts +29 -2
- package/src/plugins/builtin/pe-band/headless.ts +2 -2
- package/src/plugins/builtin/pe-band/model.ts +11 -7
- package/src/plugins/builtin/pe-band/pane.tsx +2 -1
- package/src/plugins/builtin/pe-band/report-dates.ts +78 -0
- package/src/plugins/builtin/perps/client.ts +17 -3
- package/src/plugins/builtin/perps/equity-identity.ts +16 -0
- package/src/plugins/builtin/perps/equity-row.tsx +6 -4
- package/src/plugins/builtin/plugin-marketplace/activation.ts +19 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -2
- package/src/plugins/builtin/plugin-marketplace/store.ts +2 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +43 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +48 -5
- package/src/plugins/builtin/scanner/hilo-bars.tsx +12 -70
- package/src/plugins/builtin/scanner/hilo-model.ts +0 -18
- package/src/plugins/builtin/sectors/move-bar.tsx +10 -32
- package/src/plugins/builtin/social-mentions/index.tsx +1 -1
- package/src/plugins/builtin/thirteenf/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +5 -60
- package/src/plugins/builtin/ticker-detail/overview/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +7 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +20 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/catalog-browser.ts +2 -0
- package/src/plugins/catalog-manifest.ts +1 -0
- package/src/plugins/catalog-ui.ts +3 -1
- package/src/plugins/catalog.ts +1 -1
- package/src/plugins/host-link.ts +33 -1
- package/src/plugins/installer.ts +48 -12
- package/src/plugins/loader.ts +18 -3
- package/src/plugins/manager-node.ts +15 -5
- package/src/plugins/registry/index.ts +1 -1
- package/src/plugins/registry/pane-settings.ts +25 -7
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/row.tsx +50 -16
- package/src/renderers/dom/desktop/icons.tsx +1 -0
- package/src/renderers/dom/host/box.tsx +69 -26
- package/src/renderers/dom/host/tabs.tsx +37 -1
- package/src/renderers/dom/styles.css +40 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +181 -87
- package/src/renderers/electrobun/bun/index.ts +73 -6
- package/src/renderers/electrobun/bun/window/remembered-frame.ts +122 -0
- package/src/renderers/electrobun/shared/protocol.ts +3 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +2 -1
- package/src/renderers/electrobun/view/main.tsx +7 -1
- package/src/renderers/electrobun/view/renderer-window-error.ts +18 -0
- package/src/renderers/electrobun/view/update-host.ts +45 -21
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -2
- package/src/sources/provider-router/financials.ts +3 -1
- package/src/telemetry/benign-window-error.ts +7 -0
- package/src/telemetry/crash-reports-dom.ts +2 -0
- package/src/test-support/daily-nav.ts +25 -0
- package/src/theme/heat-colors.ts +174 -1
- package/src/tickers/instrument-kind.ts +34 -7
- package/src/tickers/search/asset-classes.ts +24 -9
- package/src/tickers/search/index.ts +44 -11
- package/src/tickers/search/ranking.ts +23 -3
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +18 -5
- package/src/ui/context-menu.tsx +1 -0
- package/src/ui/host.tsx +2 -0
- package/src/updater/index.ts +29 -2
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
|
2
|
+
import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
|
|
3
|
+
import { blendHex, colors } from "../../theme/colors";
|
|
4
|
+
import { heatmapColorDistance, resolveHeatmapSelectedTileColors, resolveHeatmapTileColors } from "../../theme/heat-colors";
|
|
5
|
+
import { t } from "../../i18n";
|
|
6
|
+
import { clipToDisplayWidth, padTo } from "../../utils/format";
|
|
7
|
+
import type { FloatMetricTreemapTile, MetricTreemapItem } from "./layout";
|
|
8
|
+
import {
|
|
9
|
+
buildMetricTreemapScene,
|
|
10
|
+
type MetricTreemapHeader,
|
|
11
|
+
type MetricTreemapScene,
|
|
12
|
+
type MetricTreemapSceneOptions,
|
|
13
|
+
type TreemapRect,
|
|
14
|
+
} from "./scene";
|
|
15
|
+
import {
|
|
16
|
+
HeatPulseTracker,
|
|
17
|
+
heatTileLabelCells,
|
|
18
|
+
heatTileLabelPx,
|
|
19
|
+
heatTileMotion,
|
|
20
|
+
} from "./heat-tiles";
|
|
21
|
+
|
|
22
|
+
type PreventableMouseEvent = { preventDefault(): void };
|
|
23
|
+
type PointerEvent = { x?: number; y?: number; pixelX?: number; pixelY?: number; source?: unknown };
|
|
24
|
+
|
|
25
|
+
export interface HeatTreemapCanvas {
|
|
26
|
+
/** Layout units: px on the desktop and the web, cells in the terminal. */
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
options: MetricTreemapSceneOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NARROW_CANVAS_PX = 600;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The canvas a heat map is laid out on. The pane and the surface both build
|
|
36
|
+
* the scene from this, so keyboard navigation walks the tiles the user sees.
|
|
37
|
+
*/
|
|
38
|
+
export function heatTreemapCanvas(
|
|
39
|
+
width: number,
|
|
40
|
+
height: number,
|
|
41
|
+
capabilities: { nativePaneChrome?: boolean; cellWidthPx?: number; cellHeightPx?: number },
|
|
42
|
+
): HeatTreemapCanvas {
|
|
43
|
+
const cellWidthPx = capabilities.cellWidthPx ?? 8;
|
|
44
|
+
const cellHeightPx = capabilities.cellHeightPx ?? 18;
|
|
45
|
+
if (capabilities.nativePaneChrome) {
|
|
46
|
+
const pxWidth = Math.max(1, width * cellWidthPx);
|
|
47
|
+
const pxHeight = Math.max(1, height * cellHeightPx);
|
|
48
|
+
const narrow = pxWidth < NARROW_CANVAS_PX;
|
|
49
|
+
return {
|
|
50
|
+
width: pxWidth,
|
|
51
|
+
height: pxHeight,
|
|
52
|
+
options: {
|
|
53
|
+
snap: false,
|
|
54
|
+
aspect: 1,
|
|
55
|
+
groupInset: narrow ? 1.5 : 2,
|
|
56
|
+
subgroupInset: 1,
|
|
57
|
+
headerHeight: narrow ? [14, 12] : [16, 13],
|
|
58
|
+
headerMin: [{ width: 44, height: 40 }, { width: 84, height: 56 }],
|
|
59
|
+
minTile: { width: 7, height: 7 },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
width: Math.max(1, width - 2),
|
|
65
|
+
height: Math.max(1, height),
|
|
66
|
+
options: {
|
|
67
|
+
snap: true,
|
|
68
|
+
aspect: Math.max(0.5, Math.min(4, cellHeightPx / Math.max(1, cellWidthPx))),
|
|
69
|
+
groupInset: 0,
|
|
70
|
+
subgroupInset: 0,
|
|
71
|
+
headerHeight: [1, 1],
|
|
72
|
+
headerMin: [{ width: 8, height: 4 }, { width: 16, height: 7 }],
|
|
73
|
+
minTile: { width: 3, height: 1 },
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildHeatTreemapScene<T>(
|
|
79
|
+
items: readonly MetricTreemapItem<T>[],
|
|
80
|
+
canvas: HeatTreemapCanvas,
|
|
81
|
+
previous?: MetricTreemapScene<T> | null,
|
|
82
|
+
): MetricTreemapScene<T> {
|
|
83
|
+
return buildMetricTreemapScene(items, canvas.width, canvas.height, canvas.options, previous);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface HeatTreemapSurfaceProps<T> {
|
|
87
|
+
/** Geometry, from `buildHeatTreemapScene`; rebuilt only when sizes change. */
|
|
88
|
+
scene: MetricTreemapScene<T>;
|
|
89
|
+
/** Current colours and text, by id; they change with every tick. */
|
|
90
|
+
items: readonly MetricTreemapItem<T>[];
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
selectedId: string | null;
|
|
94
|
+
onSelect: (item: MetricTreemapItem<T>) => void;
|
|
95
|
+
onActivate?: (item: MetricTreemapItem<T>) => void;
|
|
96
|
+
emptyStateTitle?: string;
|
|
97
|
+
/** Hover text for the Other block, given the names it holds. */
|
|
98
|
+
otherTooltip?: (items: readonly MetricTreemapItem<T>[]) => readonly string[];
|
|
99
|
+
/** One short line under the Other label, where there is room for it. */
|
|
100
|
+
otherSummary?: (items: readonly MetricTreemapItem<T>[]) => string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface SurfaceEvents {
|
|
104
|
+
select(id: string): void;
|
|
105
|
+
activate(id: string): void;
|
|
106
|
+
hover(id: string, event: PointerEvent | undefined, selects: boolean): void;
|
|
107
|
+
leave(id: string): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function useLatest<T>(value: T) {
|
|
111
|
+
const ref = useRef(value);
|
|
112
|
+
ref.current = value;
|
|
113
|
+
return ref;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Hover selects the tile under the pointer only when the pointer really moved:
|
|
118
|
+
* a relayout under a resting pointer must not take the keyboard's selection.
|
|
119
|
+
*/
|
|
120
|
+
function usePointerMoved(): (event: PointerEvent | undefined) => boolean {
|
|
121
|
+
const lastRef = useRef<string | null>(null);
|
|
122
|
+
return useMemo(() => (event: PointerEvent | undefined) => {
|
|
123
|
+
const x = event?.pixelX ?? event?.x;
|
|
124
|
+
const y = event?.pixelY ?? event?.y;
|
|
125
|
+
if (typeof x !== "number" || typeof y !== "number") return true;
|
|
126
|
+
const position = `${x}:${y}`;
|
|
127
|
+
if (lastRef.current === position) return false;
|
|
128
|
+
lastRef.current = position;
|
|
129
|
+
return true;
|
|
130
|
+
}, []);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface MediaQueryLike {
|
|
134
|
+
matches: boolean;
|
|
135
|
+
addEventListener?(type: "change", listener: () => void): void;
|
|
136
|
+
removeEventListener?(type: "change", listener: () => void): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function reducedMotionQuery(): MediaQueryLike | null {
|
|
140
|
+
const matchMedia = (globalThis as { matchMedia?: (query: string) => MediaQueryLike }).matchMedia;
|
|
141
|
+
return typeof matchMedia === "function" ? matchMedia.call(globalThis, "(prefers-reduced-motion: reduce)") : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function usePrefersReducedMotion(): boolean {
|
|
145
|
+
const [reduced, setReduced] = useState(() => reducedMotionQuery()?.matches === true);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const query = reducedMotionQuery();
|
|
148
|
+
if (!query) return;
|
|
149
|
+
const update = () => setReduced(query.matches);
|
|
150
|
+
update();
|
|
151
|
+
query.addEventListener?.("change", update);
|
|
152
|
+
return () => query.removeEventListener?.("change", update);
|
|
153
|
+
}, []);
|
|
154
|
+
return reduced;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function useSurfaceEvents<T>(
|
|
158
|
+
itemsById: ReadonlyMap<string, MetricTreemapItem<T>>,
|
|
159
|
+
onSelect: (item: MetricTreemapItem<T>) => void,
|
|
160
|
+
onActivate: ((item: MetricTreemapItem<T>) => void) | undefined,
|
|
161
|
+
onHover?: (id: string | null, event: PointerEvent | undefined) => void,
|
|
162
|
+
): SurfaceEvents {
|
|
163
|
+
const latest = useLatest({ itemsById, onSelect, onActivate, onHover });
|
|
164
|
+
const pointerMoved = usePointerMoved();
|
|
165
|
+
return useMemo<SurfaceEvents>(() => ({
|
|
166
|
+
select(id) {
|
|
167
|
+
const item = latest.current.itemsById.get(id);
|
|
168
|
+
if (item) latest.current.onSelect(item);
|
|
169
|
+
},
|
|
170
|
+
activate(id) {
|
|
171
|
+
const item = latest.current.itemsById.get(id);
|
|
172
|
+
if (item) latest.current.onActivate?.(item);
|
|
173
|
+
},
|
|
174
|
+
hover(id, event, selects) {
|
|
175
|
+
latest.current.onHover?.(id, event);
|
|
176
|
+
if (!selects || !pointerMoved(event)) return;
|
|
177
|
+
const item = latest.current.itemsById.get(id);
|
|
178
|
+
if (item) latest.current.onSelect(item);
|
|
179
|
+
},
|
|
180
|
+
leave(id) {
|
|
181
|
+
latest.current.onHover?.(null, undefined);
|
|
182
|
+
void id;
|
|
183
|
+
},
|
|
184
|
+
}), [latest, pointerMoved]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pct(value: number, total: number): string {
|
|
188
|
+
return `${total > 0 ? value / total * 100 : 0}%`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function useItemsById<T>(scene: MetricTreemapScene<T>, items: readonly MetricTreemapItem<T>[]) {
|
|
192
|
+
return useMemo(() => {
|
|
193
|
+
const byId = new Map<string, MetricTreemapItem<T>>();
|
|
194
|
+
for (const tile of scene.tiles) byId.set(tile.item.id, tile.item);
|
|
195
|
+
for (const item of items) byId.set(item.id, item);
|
|
196
|
+
return byId;
|
|
197
|
+
}, [items, scene]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Desktop and web: real DOM boxes, CSS transitions for fades and glides.
|
|
202
|
+
|
|
203
|
+
const VISIBLE_FADE_DISTANCE = 0.03;
|
|
204
|
+
|
|
205
|
+
const DomHeatTile = memo(function DomHeatTile({
|
|
206
|
+
id, rect, canvasWidth, canvasHeight, gap, label, value, background, foreground,
|
|
207
|
+
selected, reducedMotion, glide, pulseCount, events,
|
|
208
|
+
}: {
|
|
209
|
+
id: string;
|
|
210
|
+
rect: TreemapRect;
|
|
211
|
+
canvasWidth: number;
|
|
212
|
+
canvasHeight: number;
|
|
213
|
+
gap: number;
|
|
214
|
+
label: string;
|
|
215
|
+
value: string | null;
|
|
216
|
+
background: string;
|
|
217
|
+
foreground: string;
|
|
218
|
+
selected: boolean;
|
|
219
|
+
reducedMotion: boolean;
|
|
220
|
+
glide: boolean;
|
|
221
|
+
pulseCount: number;
|
|
222
|
+
events: SurfaceEvents;
|
|
223
|
+
}) {
|
|
224
|
+
const text = heatTileLabelPx(rect.width - gap * 2, rect.height - gap * 2, label, value);
|
|
225
|
+
// Where this tile sat before the last relayout, kept to slide it from there.
|
|
226
|
+
const placedRef = useRef(rect);
|
|
227
|
+
const glideRef = useRef<{ dx: number; dy: number; count: number }>({ dx: 0, dy: 0, count: 0 });
|
|
228
|
+
if (placedRef.current !== rect) {
|
|
229
|
+
const dx = placedRef.current.x - rect.x;
|
|
230
|
+
const dy = placedRef.current.y - rect.y;
|
|
231
|
+
if (glide && (Math.abs(dx) >= 0.5 || Math.abs(dy) >= 0.5)) {
|
|
232
|
+
glideRef.current = { dx, dy, count: glideRef.current.count + 1 };
|
|
233
|
+
}
|
|
234
|
+
placedRef.current = rect;
|
|
235
|
+
}
|
|
236
|
+
const motion = heatTileMotion({ reducedMotion, glide, glideCount: glideRef.current.count, pulseCount });
|
|
237
|
+
// The colour this tile showed before, kept to fade out over the new one.
|
|
238
|
+
const shownRef = useRef(background);
|
|
239
|
+
const fadeRef = useRef<{ from: string; key: number } | null>(null);
|
|
240
|
+
if (shownRef.current !== background) {
|
|
241
|
+
// A step too small to see snaps: a fade costs a compositor layer for its whole run.
|
|
242
|
+
const visible = heatmapColorDistance(shownRef.current, background) >= VISIBLE_FADE_DISTANCE;
|
|
243
|
+
if (!motion.fadeAnimation) fadeRef.current = null;
|
|
244
|
+
else if (visible) fadeRef.current = { from: shownRef.current, key: (fadeRef.current?.key ?? 0) + 1 };
|
|
245
|
+
shownRef.current = background;
|
|
246
|
+
}
|
|
247
|
+
const fade = fadeRef.current;
|
|
248
|
+
// A finished overlay leaves the DOM, so idle tiles carry no extra nodes.
|
|
249
|
+
const [, setOverlayVersion] = useState(0);
|
|
250
|
+
const pulseDoneRef = useRef(0);
|
|
251
|
+
const pulsing = motion.pulseAnimation != null && pulseCount > pulseDoneRef.current;
|
|
252
|
+
const style: CSSProperties = {
|
|
253
|
+
position: "absolute",
|
|
254
|
+
left: `calc(${pct(rect.x, canvasWidth)} + ${gap}px)`,
|
|
255
|
+
top: `calc(${pct(rect.y, canvasHeight)} + ${gap}px)`,
|
|
256
|
+
width: `max(1px, calc(${pct(rect.width, canvasWidth)} - ${gap * 2}px))`,
|
|
257
|
+
height: `max(1px, calc(${pct(rect.height, canvasHeight)} - ${gap * 2}px))`,
|
|
258
|
+
backgroundColor: background,
|
|
259
|
+
color: foreground,
|
|
260
|
+
borderRadius: 1,
|
|
261
|
+
overflow: "hidden",
|
|
262
|
+
// A tick restyles one tile; containment keeps it from re-laying out or repainting its neighbours.
|
|
263
|
+
contain: "strict",
|
|
264
|
+
display: "flex",
|
|
265
|
+
flexDirection: "column",
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
justifyContent: "center",
|
|
268
|
+
gap: 0,
|
|
269
|
+
cursor: "pointer",
|
|
270
|
+
// The page's brightest text, then a hairline of the page: clear on a pink or
|
|
271
|
+
// aqua tile whether its own text is dark or light.
|
|
272
|
+
boxShadow: selected ? `inset 0 0 0 2px ${colors.textBright}, inset 0 0 0 3px ${colors.bg}` : undefined,
|
|
273
|
+
zIndex: selected ? 1 : undefined,
|
|
274
|
+
...(motion.glideAnimation
|
|
275
|
+
? {
|
|
276
|
+
"--heat-glide-x": `${glideRef.current.dx}px`,
|
|
277
|
+
"--heat-glide-y": `${glideRef.current.dy}px`,
|
|
278
|
+
animation: motion.glideAnimation,
|
|
279
|
+
} as CSSProperties
|
|
280
|
+
: {}),
|
|
281
|
+
};
|
|
282
|
+
const overlayStyle: CSSProperties = {
|
|
283
|
+
position: "absolute",
|
|
284
|
+
left: 0,
|
|
285
|
+
top: 0,
|
|
286
|
+
width: "100%",
|
|
287
|
+
height: "100%",
|
|
288
|
+
pointerEvents: "none",
|
|
289
|
+
};
|
|
290
|
+
const lineStyle: CSSProperties = {
|
|
291
|
+
position: "relative",
|
|
292
|
+
display: "block",
|
|
293
|
+
maxWidth: "100%",
|
|
294
|
+
overflow: "hidden",
|
|
295
|
+
whiteSpace: "nowrap",
|
|
296
|
+
textAlign: "center",
|
|
297
|
+
letterSpacing: 0,
|
|
298
|
+
color: foreground,
|
|
299
|
+
};
|
|
300
|
+
return (
|
|
301
|
+
<Box
|
|
302
|
+
data-gloom-role="heat-tile"
|
|
303
|
+
style={style}
|
|
304
|
+
onMouseDown={(event: PreventableMouseEvent) => {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
events.select(id);
|
|
307
|
+
}}
|
|
308
|
+
onDoubleClick={() => events.activate(id)}
|
|
309
|
+
onMouseOver={(event: PointerEvent) => events.hover(id, event, false)}
|
|
310
|
+
onMouseMove={(event: PointerEvent) => events.hover(id, event, true)}
|
|
311
|
+
onMouseOut={() => events.leave(id)}
|
|
312
|
+
>
|
|
313
|
+
{fade && (
|
|
314
|
+
<Box
|
|
315
|
+
key={`fade:${fade.key}`}
|
|
316
|
+
// Invisible at rest: if animations are off the old colour must not stay on top.
|
|
317
|
+
style={{ ...overlayStyle, backgroundColor: fade.from, opacity: 0, animation: motion.fadeAnimation }}
|
|
318
|
+
onAnimationEnd={() => {
|
|
319
|
+
if (fadeRef.current?.key !== fade.key) return;
|
|
320
|
+
fadeRef.current = null;
|
|
321
|
+
setOverlayVersion((version) => version + 1);
|
|
322
|
+
}}
|
|
323
|
+
/>
|
|
324
|
+
)}
|
|
325
|
+
{pulsing && (
|
|
326
|
+
<Box
|
|
327
|
+
key={`pulse:${pulseCount}`}
|
|
328
|
+
style={{ ...overlayStyle, backgroundColor: "#ffffff", opacity: 0, animation: motion.pulseAnimation }}
|
|
329
|
+
onAnimationEnd={() => {
|
|
330
|
+
pulseDoneRef.current = Math.max(pulseDoneRef.current, pulseCount);
|
|
331
|
+
setOverlayVersion((version) => version + 1);
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
335
|
+
{text.tier !== "none" && (
|
|
336
|
+
<Text style={{ ...lineStyle, fontSize: text.tickerPx, fontWeight: 700, lineHeight: 1.12 }}>{label}</Text>
|
|
337
|
+
)}
|
|
338
|
+
{text.tier === "full" && value && (
|
|
339
|
+
<Text style={{ ...lineStyle, fontSize: text.valuePx, fontWeight: 500, lineHeight: 1.12 }}>{value}</Text>
|
|
340
|
+
)}
|
|
341
|
+
</Box>
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
function headerLabel(header: MetricTreemapHeader): string {
|
|
346
|
+
return header.other ? t("Other") : header.label;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function DomHeader({ header, canvasWidth, canvasHeight }: { header: MetricTreemapHeader; canvasWidth: number; canvasHeight: number }) {
|
|
350
|
+
const sector = header.level === 0 && !header.other;
|
|
351
|
+
return (
|
|
352
|
+
<Box
|
|
353
|
+
data-gloom-role="heat-header"
|
|
354
|
+
style={{
|
|
355
|
+
position: "absolute",
|
|
356
|
+
left: pct(header.rect.x, canvasWidth),
|
|
357
|
+
top: pct(header.rect.y, canvasHeight),
|
|
358
|
+
width: pct(header.rect.width, canvasWidth),
|
|
359
|
+
height: pct(header.rect.height, canvasHeight),
|
|
360
|
+
display: "flex",
|
|
361
|
+
flexDirection: "row",
|
|
362
|
+
alignItems: "center",
|
|
363
|
+
justifyContent: "flex-start",
|
|
364
|
+
paddingLeft: 2,
|
|
365
|
+
overflow: "hidden",
|
|
366
|
+
pointerEvents: "none",
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
<Text
|
|
370
|
+
style={{
|
|
371
|
+
display: "block",
|
|
372
|
+
minWidth: 0,
|
|
373
|
+
overflow: "hidden",
|
|
374
|
+
textOverflow: "ellipsis",
|
|
375
|
+
whiteSpace: "nowrap",
|
|
376
|
+
textTransform: "uppercase",
|
|
377
|
+
letterSpacing: "0.07em",
|
|
378
|
+
fontSize: header.level === 0 ? 10.5 : 9,
|
|
379
|
+
fontWeight: header.level === 0 ? 700 : 600,
|
|
380
|
+
lineHeight: 1,
|
|
381
|
+
color: sector ? colors.textBright : colors.textDim,
|
|
382
|
+
}}
|
|
383
|
+
>
|
|
384
|
+
{headerLabel(header)}
|
|
385
|
+
</Text>
|
|
386
|
+
</Box>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
interface TooltipState {
|
|
391
|
+
id: string;
|
|
392
|
+
x: number;
|
|
393
|
+
y: number;
|
|
394
|
+
boundsWidth: number;
|
|
395
|
+
boundsHeight: number;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const OTHER_ID = "\u0000other";
|
|
399
|
+
|
|
400
|
+
function DomTooltip<T>({ state, itemsById, otherLines }: {
|
|
401
|
+
state: TooltipState | null;
|
|
402
|
+
itemsById: ReadonlyMap<string, MetricTreemapItem<T>>;
|
|
403
|
+
otherLines: readonly string[] | null;
|
|
404
|
+
}) {
|
|
405
|
+
if (!state) return null;
|
|
406
|
+
const lines = state.id === OTHER_ID
|
|
407
|
+
? otherLines
|
|
408
|
+
: itemsById.get(state.id)?.tooltip ?? [itemsById.get(state.id)?.label ?? ""];
|
|
409
|
+
if (!lines || lines.length === 0) return null;
|
|
410
|
+
const flipX = state.x > state.boundsWidth * 0.6;
|
|
411
|
+
const flipY = state.y > state.boundsHeight * 0.72;
|
|
412
|
+
return (
|
|
413
|
+
<Box
|
|
414
|
+
data-gloom-role="heat-tooltip"
|
|
415
|
+
style={{
|
|
416
|
+
position: "absolute",
|
|
417
|
+
left: state.x + (flipX ? -12 : 14),
|
|
418
|
+
top: state.y + (flipY ? -10 : 16),
|
|
419
|
+
transform: `translate(${flipX ? "-100%" : "0"}, ${flipY ? "-100%" : "0"})`,
|
|
420
|
+
zIndex: 5,
|
|
421
|
+
pointerEvents: "none",
|
|
422
|
+
backgroundColor: blendHex(colors.bg, colors.panel, 0.5),
|
|
423
|
+
border: `1px solid ${colors.border}`,
|
|
424
|
+
borderRadius: 3,
|
|
425
|
+
padding: "4px 7px",
|
|
426
|
+
boxShadow: `0 4px 14px ${blendHex(colors.bg, "#000000", 0.6)}`,
|
|
427
|
+
display: "flex",
|
|
428
|
+
flexDirection: "column",
|
|
429
|
+
gap: 2,
|
|
430
|
+
maxWidth: 400,
|
|
431
|
+
}}
|
|
432
|
+
>
|
|
433
|
+
{lines.map((line, index) => (
|
|
434
|
+
<Text
|
|
435
|
+
key={index}
|
|
436
|
+
style={{
|
|
437
|
+
display: "block",
|
|
438
|
+
whiteSpace: "nowrap",
|
|
439
|
+
overflow: "hidden",
|
|
440
|
+
textOverflow: "ellipsis",
|
|
441
|
+
fontSize: 11.5,
|
|
442
|
+
lineHeight: 1.25,
|
|
443
|
+
fontWeight: index === 0 ? 700 : 400,
|
|
444
|
+
color: index === 0 ? colors.textBright : colors.textDim,
|
|
445
|
+
}}
|
|
446
|
+
>
|
|
447
|
+
{line}
|
|
448
|
+
</Text>
|
|
449
|
+
))}
|
|
450
|
+
</Box>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const EMPTY_PULSES: ReadonlyMap<string, number> = new Map();
|
|
455
|
+
|
|
456
|
+
function DomHeatTreemap<T>({
|
|
457
|
+
scene, items, width, height, selectedId, onSelect, onActivate, otherTooltip, otherSummary: summarizeOther,
|
|
458
|
+
}: HeatTreemapSurfaceProps<T>) {
|
|
459
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
460
|
+
const itemsById = useItemsById(scene, items);
|
|
461
|
+
const chartRef = useRef<{ getBoundingClientRect?: () => { x: number; y: number; width: number; height: number } } | null>(null);
|
|
462
|
+
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
|
463
|
+
const onHover = useMemo(() => (id: string | null, event: PointerEvent | undefined) => {
|
|
464
|
+
if (id == null) {
|
|
465
|
+
setTooltip(null);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const bounds = chartRef.current?.getBoundingClientRect?.();
|
|
469
|
+
if (!bounds || typeof event?.pixelX !== "number" || typeof event?.pixelY !== "number") return;
|
|
470
|
+
setTooltip({ id, x: event.pixelX - bounds.x, y: event.pixelY - bounds.y, boundsWidth: bounds.width, boundsHeight: bounds.height });
|
|
471
|
+
}, []);
|
|
472
|
+
const events = useSurfaceEvents(itemsById, onSelect, onActivate, onHover);
|
|
473
|
+
|
|
474
|
+
const trackerRef = useRef<HeatPulseTracker | null>(null);
|
|
475
|
+
trackerRef.current ??= new HeatPulseTracker();
|
|
476
|
+
const pulses = useMemo(() => {
|
|
477
|
+
if (reducedMotion) return EMPTY_PULSES;
|
|
478
|
+
return trackerRef.current!.update(
|
|
479
|
+
scene.tiles.map((tile) => [tile.item.id, itemsById.get(tile.item.id)?.colorValue ?? null] as const),
|
|
480
|
+
Date.now(),
|
|
481
|
+
);
|
|
482
|
+
}, [itemsById, reducedMotion, scene]);
|
|
483
|
+
|
|
484
|
+
// A resize relays out every tile; following the window with a glide would lag it.
|
|
485
|
+
const sizeKey = `${scene.width}x${scene.height}`;
|
|
486
|
+
const lastSizeRef = useRef(sizeKey);
|
|
487
|
+
const glide = lastSizeRef.current === sizeKey;
|
|
488
|
+
useEffect(() => {
|
|
489
|
+
lastSizeRef.current = sizeKey;
|
|
490
|
+
}, [sizeKey]);
|
|
491
|
+
|
|
492
|
+
const gap = scene.width < NARROW_CANVAS_PX ? 0.5 : 1;
|
|
493
|
+
const otherLines = useMemo(
|
|
494
|
+
() => (scene.other ? otherTooltip?.(scene.other.items) ?? [`${t("Other")} · ${scene.other.items.length}`] : null),
|
|
495
|
+
[otherTooltip, scene.other],
|
|
496
|
+
);
|
|
497
|
+
const other = scene.other;
|
|
498
|
+
|
|
499
|
+
return (
|
|
500
|
+
<Box
|
|
501
|
+
width={width}
|
|
502
|
+
height={height}
|
|
503
|
+
style={{ position: "relative", backgroundColor: colors.bg, overflow: "hidden" }}
|
|
504
|
+
>
|
|
505
|
+
<Box
|
|
506
|
+
ref={chartRef}
|
|
507
|
+
data-gloom-role="heat-treemap"
|
|
508
|
+
style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", contain: "strict" }}
|
|
509
|
+
>
|
|
510
|
+
{scene.headers.map((header) => (
|
|
511
|
+
<DomHeader key={header.id} header={header} canvasWidth={scene.width} canvasHeight={scene.height} />
|
|
512
|
+
))}
|
|
513
|
+
{other && (
|
|
514
|
+
<Box
|
|
515
|
+
data-gloom-role="heat-other"
|
|
516
|
+
style={{
|
|
517
|
+
position: "absolute",
|
|
518
|
+
left: pct(other.rect.x, scene.width),
|
|
519
|
+
top: pct(other.rect.y, scene.height),
|
|
520
|
+
width: pct(other.rect.width, scene.width),
|
|
521
|
+
height: pct(other.rect.height, scene.height),
|
|
522
|
+
backgroundColor: blendHex(colors.bg, colors.textMuted, 0.1),
|
|
523
|
+
borderRadius: 1,
|
|
524
|
+
overflow: "hidden",
|
|
525
|
+
paddingLeft: 3,
|
|
526
|
+
paddingTop: 2,
|
|
527
|
+
}}
|
|
528
|
+
onMouseOver={(event: PointerEvent) => onHover(OTHER_ID, event)}
|
|
529
|
+
onMouseMove={(event: PointerEvent) => onHover(OTHER_ID, event)}
|
|
530
|
+
onMouseOut={() => onHover(null, undefined)}
|
|
531
|
+
>
|
|
532
|
+
{other.showLabel && (
|
|
533
|
+
<Text style={{ display: "block", fontSize: 10.5, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", color: colors.textDim, lineHeight: 1.2 }}>
|
|
534
|
+
{t("Other")}
|
|
535
|
+
</Text>
|
|
536
|
+
)}
|
|
537
|
+
{(other.showLabel || (other.rect.width >= 44 && other.rect.height >= 24)) && summarizeOther && (
|
|
538
|
+
<Text style={{ display: "block", fontSize: 10, color: colors.textMuted, lineHeight: 1.3, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
539
|
+
{summarizeOther(other.items)}
|
|
540
|
+
</Text>
|
|
541
|
+
)}
|
|
542
|
+
</Box>
|
|
543
|
+
)}
|
|
544
|
+
{scene.tiles.map((tile) => {
|
|
545
|
+
const item = itemsById.get(tile.item.id) ?? tile.item;
|
|
546
|
+
const tileColors = resolveHeatmapTileColors(item.colorValue);
|
|
547
|
+
return (
|
|
548
|
+
<DomHeatTile
|
|
549
|
+
key={tile.item.id}
|
|
550
|
+
id={tile.item.id}
|
|
551
|
+
rect={tileRect(tile)}
|
|
552
|
+
canvasWidth={scene.width}
|
|
553
|
+
canvasHeight={scene.height}
|
|
554
|
+
gap={gap}
|
|
555
|
+
label={item.label}
|
|
556
|
+
value={item.primaryText ?? null}
|
|
557
|
+
background={tileColors.background}
|
|
558
|
+
foreground={tileColors.foreground}
|
|
559
|
+
selected={tile.item.id === selectedId}
|
|
560
|
+
reducedMotion={reducedMotion}
|
|
561
|
+
glide={glide}
|
|
562
|
+
pulseCount={pulses.get(tile.item.id) ?? 0}
|
|
563
|
+
events={events}
|
|
564
|
+
/>
|
|
565
|
+
);
|
|
566
|
+
})}
|
|
567
|
+
<DomTooltip state={tooltip} itemsById={itemsById} otherLines={otherLines} />
|
|
568
|
+
</Box>
|
|
569
|
+
</Box>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const tileRectCache = new WeakMap<object, TreemapRect>();
|
|
574
|
+
|
|
575
|
+
/** One rect object per tile, so a memoized tile sees the same prop until the layout changes. */
|
|
576
|
+
function tileRect(tile: FloatMetricTreemapTile<unknown>): TreemapRect {
|
|
577
|
+
let rect = tileRectCache.get(tile);
|
|
578
|
+
if (!rect) {
|
|
579
|
+
rect = { x: tile.x, y: tile.y, width: tile.width, height: tile.height };
|
|
580
|
+
tileRectCache.set(tile, rect);
|
|
581
|
+
}
|
|
582
|
+
return rect;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Terminal: whole cells, a one-cell gutter right of and under each tile.
|
|
587
|
+
|
|
588
|
+
const TerminalHeatTile = memo(function TerminalHeatTile({
|
|
589
|
+
id, x, y, width, height, label, value, colorValue, selected, events,
|
|
590
|
+
}: {
|
|
591
|
+
id: string;
|
|
592
|
+
x: number;
|
|
593
|
+
y: number;
|
|
594
|
+
width: number;
|
|
595
|
+
height: number;
|
|
596
|
+
label: string;
|
|
597
|
+
value: string | null;
|
|
598
|
+
colorValue: number | null;
|
|
599
|
+
selected: boolean;
|
|
600
|
+
events: SurfaceEvents;
|
|
601
|
+
}) {
|
|
602
|
+
const { background, foreground } = selected
|
|
603
|
+
? resolveHeatmapSelectedTileColors(colorValue)
|
|
604
|
+
: resolveHeatmapTileColors(colorValue);
|
|
605
|
+
const renderWidth = Math.max(1, width - (width >= 3 ? 1 : 0));
|
|
606
|
+
const renderHeight = Math.max(1, height - (height >= 2 ? 1 : 0));
|
|
607
|
+
const tier = heatTileLabelCells(renderWidth, renderHeight, label, value);
|
|
608
|
+
const lines = tier === "full" && value ? [label, value] : tier === "none" ? [] : [label];
|
|
609
|
+
const top = Math.max(0, Math.floor((renderHeight - lines.length) / 2));
|
|
610
|
+
return (
|
|
611
|
+
<Box
|
|
612
|
+
position="absolute"
|
|
613
|
+
left={x}
|
|
614
|
+
top={y}
|
|
615
|
+
width={renderWidth}
|
|
616
|
+
height={renderHeight}
|
|
617
|
+
backgroundColor={background}
|
|
618
|
+
onMouseDown={(event: PreventableMouseEvent) => {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
events.select(id);
|
|
621
|
+
}}
|
|
622
|
+
onMouseMove={(event: PointerEvent) => events.hover(id, event, true)}
|
|
623
|
+
// A held button sends no moves here, only "over" from the press it
|
|
624
|
+
// started; a relayout's "over" has no source.
|
|
625
|
+
onMouseOver={(event: PointerEvent) => {
|
|
626
|
+
if (event.source) events.hover(id, event, true);
|
|
627
|
+
}}
|
|
628
|
+
onDoubleClick={() => events.activate(id)}
|
|
629
|
+
>
|
|
630
|
+
{lines.map((line, index) => (
|
|
631
|
+
<Box key={index} position="absolute" left={0} top={top + index} width={renderWidth} height={1}>
|
|
632
|
+
<Text fg={foreground} attributes={index === 0 ? TextAttributes.BOLD : TextAttributes.NONE}>
|
|
633
|
+
{padTo(line, renderWidth, "center")}
|
|
634
|
+
</Text>
|
|
635
|
+
</Box>
|
|
636
|
+
))}
|
|
637
|
+
</Box>
|
|
638
|
+
);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
function TerminalHeatTreemap<T>({
|
|
642
|
+
scene, items, width, height, selectedId, onSelect, onActivate, otherSummary: summarizeOther,
|
|
643
|
+
}: HeatTreemapSurfaceProps<T>) {
|
|
644
|
+
const itemsById = useItemsById(scene, items);
|
|
645
|
+
const events = useSurfaceEvents(itemsById, onSelect, onActivate);
|
|
646
|
+
const other = scene.other;
|
|
647
|
+
const otherSummary = other ? summarizeOther?.(other.items) ?? null : null;
|
|
648
|
+
return (
|
|
649
|
+
<Box width={width} height={height} paddingX={1} backgroundColor={colors.bg}>
|
|
650
|
+
<Box position="relative" width={scene.width} height={scene.height} backgroundColor={colors.bg}>
|
|
651
|
+
{scene.headers.map((header) => (
|
|
652
|
+
<Box key={header.id} position="absolute" left={header.rect.x} top={header.rect.y} width={header.rect.width} height={1}>
|
|
653
|
+
<Text
|
|
654
|
+
fg={header.level === 0 && !header.other ? colors.textBright : colors.textDim}
|
|
655
|
+
attributes={header.level === 0 ? TextAttributes.BOLD : TextAttributes.NONE}
|
|
656
|
+
>
|
|
657
|
+
{clipToDisplayWidth(headerLabel(header).toUpperCase(), Math.max(0, header.rect.width - 1))}
|
|
658
|
+
</Text>
|
|
659
|
+
</Box>
|
|
660
|
+
))}
|
|
661
|
+
{other && other.rect.width > 0 && other.rect.height > 0 && (
|
|
662
|
+
<Box
|
|
663
|
+
position="absolute"
|
|
664
|
+
left={other.rect.x}
|
|
665
|
+
top={other.rect.y}
|
|
666
|
+
width={Math.max(1, other.rect.width - (other.rect.width >= 3 ? 1 : 0))}
|
|
667
|
+
height={Math.max(1, other.rect.height - (other.rect.height >= 2 ? 1 : 0))}
|
|
668
|
+
backgroundColor={colors.panel}
|
|
669
|
+
>
|
|
670
|
+
{other.showLabel && other.rect.width >= 7 && (
|
|
671
|
+
<Text fg={colors.textDim} attributes={TextAttributes.BOLD}>{clipToDisplayWidth(t("Other").toUpperCase(), other.rect.width - 1)}</Text>
|
|
672
|
+
)}
|
|
673
|
+
{/* A count is whole or absent: "2 nam…" says less than nothing. */}
|
|
674
|
+
{other.rect.height >= (other.showLabel ? 3 : 2) && otherSummary && otherSummary.length <= other.rect.width - 1 && (
|
|
675
|
+
<Text fg={colors.textMuted}>{otherSummary}</Text>
|
|
676
|
+
)}
|
|
677
|
+
</Box>
|
|
678
|
+
)}
|
|
679
|
+
{scene.tiles.map((tile) => {
|
|
680
|
+
const item = itemsById.get(tile.item.id) ?? tile.item;
|
|
681
|
+
return (
|
|
682
|
+
<TerminalHeatTile
|
|
683
|
+
key={tile.item.id}
|
|
684
|
+
id={tile.item.id}
|
|
685
|
+
x={tile.x}
|
|
686
|
+
y={tile.y}
|
|
687
|
+
width={tile.width}
|
|
688
|
+
height={tile.height}
|
|
689
|
+
label={item.label}
|
|
690
|
+
value={item.primaryText ?? null}
|
|
691
|
+
colorValue={item.colorValue ?? null}
|
|
692
|
+
selected={tile.item.id === selectedId}
|
|
693
|
+
events={events}
|
|
694
|
+
/>
|
|
695
|
+
);
|
|
696
|
+
})}
|
|
697
|
+
</Box>
|
|
698
|
+
</Box>
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* The heat map look of the treemap: optional sector and industry blocks,
|
|
704
|
+
* calm tiles (ticker, then the move, as space allows), a continuous
|
|
705
|
+
* pink-indigo-teal scale, and on the desktop a hover tooltip, colour fades,
|
|
706
|
+
* a short pulse on a tile whose move jumps and a glide when sizes change.
|
|
707
|
+
*/
|
|
708
|
+
export function HeatTreemapSurface<T>(props: HeatTreemapSurfaceProps<T>) {
|
|
709
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
710
|
+
if (props.scene.tiles.length === 0) {
|
|
711
|
+
return (
|
|
712
|
+
<Box width={props.width} height={props.height} paddingX={1} paddingY={1}>
|
|
713
|
+
<Text fg={colors.textDim}>{t(props.emptyStateTitle ?? "No chart data")}</Text>
|
|
714
|
+
</Box>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return nativePaneChrome ? <DomHeatTreemap {...props} /> : <TerminalHeatTreemap {...props} />;
|
|
718
|
+
}
|