Skip to content

Commit baf359d

Browse files
committed
add hybrid panel example
1 parent b3e90e1 commit baf359d

File tree

14 files changed

+5432
-1
lines changed

14 files changed

+5432
-1
lines changed

plugins/hello-world/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@voxel51/hello-world",
33
"version": "1.0.0",
44
"main": "src/HelloWorldPlugin.tsx",
5-
"license": "MIT",
5+
"license": "Apache 2.0",
66
"fiftyone": {
77
"script": "dist/index.umd.js"
88
},

plugins/hybrid-panel/README.md

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
# Hybrid Panel
2+
3+
A hybrid panel is a design pattern in the FiftyOne plugins framework that
4+
enables you to build rich, highly interactive UI panels by combining both
5+
Python and JavaScript/React. This approach allows complex logic, data
6+
processing, and session integration to run in Python, while the frontend
7+
delivers a responsive, dynamic user experience—giving you the best of both
8+
worlds when creating advanced custom panels.
9+
10+
## JavaScript (React)
11+
12+
On the JavaScript side of a hybrid panel, you build the UI as a component
13+
plugin, implementing the React interface that defines how the panel looks and
14+
behaves. Once created, this component is registered so that the Python side can
15+
reference it as the panel’s view. In this layer, you render elements, read and
16+
update state, respond to user interactions, and consume data generated by
17+
Python events. The JavaScript side is responsible for delivering a smooth,
18+
interactive experience, while relying on Python for configuration and heavy
19+
backend logic.
20+
21+
### Example
22+
23+
```js
24+
import { PluginComponentType, registerComponent } from "@fiftyone/plugins";
25+
26+
function MyCustomView(props) {
27+
return <strong>MyCustomView</strong>;
28+
}
29+
30+
registerComponent({
31+
name: "MyCustomView", // Used in python example below
32+
label: "MyCustomView",
33+
component: MyCustomView,
34+
type: PluginComponentType.Component,
35+
activator: () => true,
36+
});
37+
```
38+
39+
## Python
40+
41+
The Python side of a hybrid panel handles all the foundational setup and
42+
backend logic. It registers the panel, defines its basic configuration (such as
43+
the label, icon, and where it appears in the interface), and initializes its
44+
state when the panel loads. Python also provides the panel’s event
45+
handlers—effectively the panel’s API—allowing the UI to trigger computations,
46+
retrieve data, and react to user interactions.
47+
48+
### Example
49+
50+
```py
51+
import fiftyone.operators as foo
52+
import fiftyone.operators.types as types
53+
54+
class HybridPanel(foo.Panel):
55+
@property
56+
def config(self):
57+
return foo.PanelConfig(
58+
name="hybrid_panel",
59+
label="Hybrid Panel",
60+
icon="adjust",
61+
surfaces="grid modal",
62+
)
63+
64+
def on_load(self, ctx):
65+
# on_load_logic...
66+
67+
def render(self, ctx):
68+
panel = types.Object()
69+
return types.Property(
70+
panel,
71+
view=types.View(component="MyCustomView", composite_view=True),
72+
)
73+
74+
def register(p):
75+
p.register(HybridPanel)
76+
```
77+
78+
## State vs Data
79+
80+
Hybrid panels in FiftyOne support two forms of data storage: state and data.
81+
Although they work together, they serve very different purposes and should be
82+
used accordingly when building custom panels.
83+
84+
A simple mental model:
85+
86+
- State = configuration
87+
- Data = results derived from configuration
88+
89+
In a typical hybrid panel lifecycle:
90+
91+
- A user updates state (e.g., selects a label field and score threshold).
92+
- A Python callback reacts to that state change.
93+
- The callback computes data (e.g., a filtered list of detections).
94+
- The panel renders using both the lightweight state and the heavy data.
95+
96+
### State: Small, Persistent Configuration
97+
98+
State is the place to store lightweight configuration that represents the
99+
panel’s intent rather than its computed results. Think of state as:
100+
101+
- Small: simple values, flags, selected options, input parameters, panel
102+
settings
103+
104+
- Persistent: survives page refreshes and browser reloads. In the open-source
105+
app (OSS), state values stay in sync with the underlying Python session via
106+
session.spaces.
107+
108+
- Synchronized: state is available to all panel events including on_load,
109+
on_change_view, and any custom panel events you define.
110+
111+
State should not store anything large. In most cases, it should only store what
112+
the user chooses or configures.
113+
114+
Typical examples of state:
115+
116+
- Filters (label type, score threshold, tag selections)
117+
- UI selections (checkboxes, dropdown values)
118+
- Configuration options (which algorithm to run, which view field to inspect)
119+
- State represents what the user wants.
120+
121+
### Data: Large, Derived, and Computed Values
122+
123+
Data is designed for storing heavy, computed, or dynamic results—anything that
124+
is too large or too volatile to live in state. Data values are not intended to
125+
be:
126+
127+
- Sent back and forth frequently between browser and Python
128+
- Not persisted after a page reload
129+
- Typically derived from state, acting as the output of computation or a
130+
resolved value
131+
132+
Data is typically used for large or expensive values such as:
133+
134+
- Precomputed arrays, lists, or mappings
135+
- Computation outputs (e.g., results of running a model or applying a filter)
136+
- Cached processed view information
137+
138+
In most cases, data represents what the panel computes based on state.
139+
140+
### Using state and data on Python side
141+
142+
```py
143+
def increment(self, ctx):
144+
# Note: there is no ctx.panel.get_data as only state is available in events
145+
count = ctx.panel.get_state("count") or 0
146+
count = count + 1
147+
ctx.panel.set_state("count", count)
148+
ctx.panel.set_data("double_count", count * 2)
149+
```
150+
151+
### Using state and data on JavaScript side
152+
153+
```js
154+
import { usePanelStatePartial } from "@fiftyone/spaces";
155+
156+
function MyCustomView(props) {
157+
// state + data is merged and is available in data prop
158+
const { data } = props;
159+
160+
// Alternatively, the state can also be accessed and mutated using hook
161+
const defaultState = {};
162+
const localOnlyState = true;
163+
// Note: setState will update the latest state sent for the next panel event
164+
const stateKey = "state";
165+
const [state, setState] = usePanelStatePartial(stateKey, defaultState);
166+
// Note: setData updated will not be available to python side (local-only)
167+
const dataKey = "data";
168+
const [data, setData] = usePanelStatePartial(
169+
dataKey,
170+
defaultState,
171+
localOnlyState
172+
);
173+
}
174+
```
175+
176+
### Avoid Using React.useState for Persistent Panel State
177+
178+
In panels, including hybrid panels and custom components, any data stored in
179+
local `React.useState` is reset whenever the panel is moved, split, navigated
180+
away from, or when the entire app is reloaded in the browser. Because local
181+
state does not persist across these transitions, it’s recommended to use spaces
182+
state hooks (i,e `usePanelStatePartial`) for any values that need to remain
183+
stable and persistent.
184+
185+
## Panel Events
186+
187+
Panel events are the callbacks that drive a hybrid panel’s interactivity,
188+
allowing the frontend to communicate with Python whenever something meaningful
189+
happens. These events—such as on_load, on_change_view, or any custom events you
190+
define—act like the panel’s API, letting the UI trigger computations, update
191+
state, refresh data, or react to changes in the FiftyOne session. They provide
192+
the backbone of the panel’s logic flow, ensuring that user actions and
193+
application state stay in sync across Python and React.
194+
195+
### Python: Defining amd providing panel event to custom view
196+
197+
The Python side of a hybrid panel is where you define the panel’s event
198+
handlers. Events are functions that run in response to lifecycle events and
199+
user interactions. These events act as the panel’s backend logic layer,
200+
allowing you to initialize state, react to view changes, perform computations,
201+
and send results back to the UI. By defining these events in Python, you give
202+
your panel the ability to handle complex operations while keeping the frontend
203+
lightweight and responsive.
204+
205+
#### Example
206+
207+
```py
208+
209+
def on_load(self, ctx):
210+
count = ctx.panel.get_state("count") or 0
211+
ctx.panel.set_state("count", count)
212+
213+
def increment(self, ctx):
214+
count = ctx.panel.get_state("count")
215+
if count is None
216+
ctx.panel.set_state("count", count)
217+
else:
218+
ctx.panel.set_state("count", count + 1)
219+
220+
def set_double_count(self, ctx):
221+
count = ctx.params("count")
222+
ctx.panel.set_state("count", count)
223+
224+
def render(self, ctx):
225+
panel = types.Object()
226+
return types.Property(
227+
panel,
228+
view=types.View(
229+
component="MyCustomView",
230+
composite_view=True,
231+
increment=self.increment,
232+
set_double_count=self.set_double_count
233+
),
234+
)
235+
236+
```
237+
238+
### JS: Invoking panel events
239+
240+
From your React components, you can trigger these events whenever the user
241+
interacts with the UI—such as clicking a button, changing a selection, or
242+
updating a filter. Invoking a panel event sends the relevant state or
243+
parameters to Python, executes the corresponding logic, and can return results
244+
or updates back to the frontend. This allows your JavaScript code to remain
245+
lightweight while leveraging Python for computation, data processing, and
246+
complex panel behavior.
247+
248+
#### Example
249+
250+
```js
251+
import {Stack, Button, Typography} from "@mui/material"
252+
253+
function MyCustomView(props) {
254+
const {data, schema} = props
255+
const { increment, set_double_count } = schema.view
256+
const { count } = data
257+
const triggerPanelEvent = useTriggerPanelEvent();
258+
259+
const handleSetDoubleCountCallback = (response) => {
260+
const {result, error} = response
261+
console.log(result)
262+
console.log(error)
263+
}
264+
const handleSetDoubleCount = () => {
265+
const event = set_double_count
266+
const params = {count}
267+
const promptForInput = false // not applicable to panel events
268+
triggerPanelEvent(
269+
event,
270+
params,
271+
promptForInput,
272+
handleSetDoubleCountCallback
273+
);
274+
}
275+
const handleIncrement = () => {
276+
const event = increment
277+
triggerPanelEvent(event);
278+
}
279+
280+
return (
281+
<Stack direction="row" spacing={1}>
282+
<Typography>{count}</Typography>
283+
<Button onClick={() => {
284+
handleIncrement()
285+
handleSetDoubleCount()
286+
}}>
287+
</Stack>;
288+
)
289+
}
290+
```
291+
292+
## Database persistence using the ExecutionStore
293+
294+
In Hybrid Panels, you can store and retrieve panel state or results in a
295+
persistent backend. The ExecutionStore provides a way to save panel outputs,
296+
configurations, or intermediate computations to the database, ensuring that
297+
important data is permanently persisted. By using the ExecutionStore, panels
298+
can maintain continuity, cache expensive computations, and provide a consistent
299+
experience across user sessions without relying solely on ephemeral frontend
300+
state.
301+
302+
### Example
303+
304+
```py
305+
306+
def save_count(self, ctx):
307+
store = ctx.store("hybrid_panel") # create or retrieve store for this panel
308+
count = ctx.params("count")
309+
store.set("saved_count", count)
310+
ctx.ops.notify(
311+
f"Saved count has been updated to: {count}",
312+
variant="success",
313+
)
314+
```
315+
316+
## Caching using ExecutionCache
317+
318+
Hybrid panels can temporarily store computed results for faster access and
319+
improved performance. The ExecutionCache allows panels to keep intermediate
320+
data in memory or database, reducing the need to recompute expensive operations
321+
each time the panel updates or the user interacts with the UI. By leveraging
322+
this cache, panels can deliver a more responsive experience while still relying
323+
on Python for heavy computations.
324+
325+
### Example
326+
327+
```py
328+
@execution_cache
329+
def get_fibonacci_number_cacheable(self, ctx):
330+
n = ctx.params("count")
331+
return get_fibonacci_number(n)
332+
```
333+
334+
## More examples
335+
336+
### Model Evaluation Panel
337+
338+
#### Python
339+
340+
- https://github.com/voxel51/fiftyone/tree/develop/plugins/panels/model_evaluation
341+
- https://github.com/voxel51/fiftyone/tree/develop/plugins/operators/model_evaluation
342+
343+
#### JavaScript
344+
345+
- https://github.com/voxel51/fiftyone/tree/develop/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView

0 commit comments

Comments
 (0)