Skip to content

Commit b1e4a9f

Browse files
feat(widgets) Add StatsWidget (#9623)
1 parent d000516 commit b1e4a9f

File tree

5 files changed

+200
-3
lines changed

5 files changed

+200
-3
lines changed

modules/widgets/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"preact": "^10.17.0"
4646
},
4747
"peerDependencies": {
48-
"@deck.gl/core": "^9.1.0"
48+
"@deck.gl/core": "^9.1.0",
49+
"@luma.gl/core": "^9.1.9"
4950
},
5051
"gitHead": "13ace64fc2cee08c133afc882fc307253489a4e4"
5152
}

modules/widgets/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {LoadingWidget as _LoadingWidget} from './loading-widget';
1919
export {FpsWidget as _FpsWidget} from './fps-widget';
2020
export {ThemeWidget as _ThemeWidget} from './theme-widget';
2121
export {InfoWidget as _InfoWidget} from './info-widget';
22+
export {StatsWidget as _StatsWidget} from './stats-widget';
2223
export {ContextMenuWidget as _ContextMenuWidget} from './context-menu-widget';
2324
export {SplitterWidget as _SplitterWidget} from './splitter-widget';
2425
export {TimelineWidget as _TimelineWidget} from './timeline-widget';
@@ -35,6 +36,7 @@ export type {FpsWidgetProps} from './fps-widget';
3536
export type {ScaleWidgetProps} from './scale-widget';
3637
export type {ThemeWidgetProps} from './theme-widget';
3738
export type {InfoWidgetProps} from './info-widget';
39+
export type {StatsWidgetProps} from './stats-widget';
3840
export type {ContextMenuWidgetProps} from './context-menu-widget';
3941
export type {SplitterWidgetProps} from './splitter-widget';
4042
export type {TimelineWidgetProps} from './timeline-widget';
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {Widget, WidgetPlacement, WidgetProps} from '@deck.gl/core';
2+
import {luma} from '@luma.gl/core';
3+
import {render} from 'preact';
4+
import type {Stats, Stat} from '@probe.gl/stats';
5+
6+
const RIGHT_ARROW = '\u25b6';
7+
const DOWN_ARROW = '\u2b07';
8+
9+
const DEFAULT_COUNT_FORMATTER = (stat: Stat): string => `${stat.name}: ${stat.count}`;
10+
11+
function formatTime(time: number): string {
12+
return time < 1000 ? `${time.toFixed(2)}ms` : `${(time / 1000).toFixed(2)}s`;
13+
}
14+
15+
function formatMemory(bytes: number): string {
16+
const mb = bytes / 1e6;
17+
return `${mb.toFixed(1)} MB`;
18+
}
19+
20+
export const DEFAULT_FORMATTERS: Record<string, (stat: Stat) => string> = {
21+
count: DEFAULT_COUNT_FORMATTER,
22+
averageTime: (stat: Stat) => `${stat.name}: ${formatTime(stat.getAverageTime())}`,
23+
totalTime: (stat: Stat) => `${stat.name}: ${formatTime(stat.time)}`,
24+
fps: (stat: Stat) => `${stat.name}: ${Math.round(stat.getHz())}fps`,
25+
memory: (stat: Stat) => `${stat.name}: ${formatMemory(stat.count)}`
26+
};
27+
28+
export type StatsWidgetProps = WidgetProps & {
29+
type?: 'deck' | 'luma' | 'device' | 'custom';
30+
/** Stats object to visualize. */
31+
stats?: Stats;
32+
/** Title shown in the header of the pop-up. Defaults to stats.id. */
33+
title?: string;
34+
/** How many redraws to wait between updates. */
35+
framesPerUpdate?: number;
36+
/** Custom formatters for stat values. */
37+
formatters?: Record<string, string | ((stat: Stat) => string)>;
38+
/** Whether to reset particular stats after each update. */
39+
resetOnUpdate?: Record<string, boolean>;
40+
};
41+
42+
/** Displays probe.gl stats in a floating pop-up. */
43+
export class StatsWidget extends Widget<StatsWidgetProps> {
44+
static defaultProps: Required<StatsWidgetProps> = {
45+
...Widget.defaultProps,
46+
type: 'deck',
47+
stats: undefined!,
48+
title: 'Stats',
49+
framesPerUpdate: 1,
50+
formatters: {},
51+
resetOnUpdate: {},
52+
id: 'stats'
53+
};
54+
55+
className = 'deck-widget-stats';
56+
placement = 'top-left' as WidgetPlacement;
57+
58+
private _counter = 0;
59+
private _formatters: Record<string, (stat: Stat) => string>;
60+
private _resetOnUpdate: Record<string, boolean>;
61+
collapsed: boolean = true;
62+
_stats: Stats;
63+
64+
constructor(props: StatsWidgetProps = {}) {
65+
super(props, StatsWidget.defaultProps);
66+
this._formatters = {...DEFAULT_FORMATTERS};
67+
this.setProps(props);
68+
this._resetOnUpdate = {...this.props.resetOnUpdate};
69+
this._stats = this.props.stats;
70+
}
71+
72+
setProps(props: Partial<StatsWidgetProps>): void {
73+
super.setProps(props);
74+
this._stats = this._getStats();
75+
if (props.formatters) {
76+
for (const name in props.formatters) {
77+
const f = props.formatters[name];
78+
this._formatters[name] =
79+
typeof f === 'string' ? DEFAULT_FORMATTERS[f] || DEFAULT_COUNT_FORMATTER : f;
80+
}
81+
}
82+
if (props.resetOnUpdate) {
83+
this._resetOnUpdate = {...props.resetOnUpdate};
84+
}
85+
}
86+
87+
onAdd(): void {
88+
this._stats = this._getStats();
89+
this.updateHTML();
90+
}
91+
92+
onRenderHTML(rootElement: HTMLElement): void {
93+
const stats = this._stats;
94+
const collapsed = this.collapsed;
95+
const title = this.props.title || stats?.id || 'Stats';
96+
const items: JSX.Element[] = [];
97+
98+
if (!collapsed && stats) {
99+
stats.forEach(stat => {
100+
const lines = this._getLines(stat);
101+
if (this._resetOnUpdate && this._resetOnUpdate[stat.name]) {
102+
stat.reset();
103+
}
104+
lines.forEach((line, i) => {
105+
items.push(
106+
<div key={`${stat.name}-${i}`} style={{whiteSpace: 'pre'}}>
107+
{line}
108+
</div>
109+
);
110+
});
111+
});
112+
}
113+
114+
render(
115+
<div className="deck-widget-stats-container" style={{cursor: 'default'}}>
116+
<div
117+
className="deck-widget-stats-header"
118+
style={{cursor: 'pointer', pointerEvents: 'auto'}}
119+
onClick={this._toggleCollapsed}
120+
>
121+
{collapsed ? RIGHT_ARROW : DOWN_ARROW} {title}
122+
</div>
123+
{!collapsed && <div className="deck-widget-stats-content deck-widget-common">{items}</div>}
124+
</div>,
125+
rootElement
126+
);
127+
}
128+
129+
onRedraw(): void {
130+
const framesPerUpdate = Math.max(1, this.props.framesPerUpdate || 1);
131+
if (this._counter++ % framesPerUpdate === 0) {
132+
console.log('Redrawing stats widget');
133+
this.updateHTML();
134+
}
135+
}
136+
137+
protected _getStats(): Stats {
138+
switch (this.props.type) {
139+
case 'deck':
140+
// @ts-expect-error stats is protected
141+
return this.deck?.stats;
142+
case 'luma':
143+
return Array.from(luma.stats.stats.values())[0];
144+
case 'device':
145+
// @ts-expect-error is protected
146+
const device = this.deck?.device;
147+
const stats = device?.statsManager.stats.values();
148+
return stats ? Array.from(stats)[0] : undefined;
149+
case 'custom':
150+
return this.props.stats;
151+
default:
152+
throw new Error(`Unknown stats type: ${this.props.type}`);
153+
}
154+
}
155+
156+
protected _toggleCollapsed = (): void => {
157+
this.collapsed = !this.collapsed;
158+
this.updateHTML();
159+
};
160+
161+
protected _getLines(stat: Stat): string[] {
162+
const formatter =
163+
this._formatters[stat.name] || this._formatters[stat.type || ''] || DEFAULT_COUNT_FORMATTER;
164+
return formatter(stat).split('\n');
165+
}
166+
}

modules/widgets/src/stylesheet.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,29 @@
250250
pointer-events: auto;
251251
cursor: pointer;
252252
}
253+
254+
/**
255+
* Reusable styles for widgets
256+
*/
257+
.deck-widget-common {
258+
font-family:
259+
'SF Mono',
260+
/* macOS */
261+
'Menlo',
262+
/* macOS */
263+
'Consolas',
264+
/* Windows */
265+
'DejaVu Sans Mono',
266+
/* Linux */
267+
'Liberation Mono',
268+
/* Linux */
269+
monospace;
270+
background-color: var(--button-background, #fff);
271+
color: #333;
272+
}
273+
274+
.deck-widget-stats-container {
275+
font-size: 0.7rem;
276+
/* Approximately 14px */
277+
line-height: 1.6;
278+
}

test/apps/widgets-example-9.2/app.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
_SplitterWidget,
2121
_TimelineWidget,
2222
_ViewSelectorWidget,
23-
_ContextMenuWidget
23+
_ContextMenuWidget,
24+
_StatsWidget
2425
} from '@deck.gl/widgets';
2526
import '@deck.gl/widgets/stylesheet.css';
2627

@@ -136,7 +137,8 @@ const deck = new Deck({
136137
viewId2: 'right-map',
137138
orientation: 'vertical',
138139
onChange: ratio => deck.setProps({views: getViewsForSplit(ratio * 100)})
139-
})
140+
}),
141+
new _StatsWidget({type: 'luma'})
140142
]
141143
});
142144

0 commit comments

Comments
 (0)