Creating a New View
This guide walks you through the process of creating a new view in figpack. Views are the fundamental building blocks for data visualization in figpack. While some general-purpose views are built into the figpack-figure application, you’ll often want to create custom views within an extension.
Prerequisites
Before creating a new view, ensure you have:
Basic understanding of Python and React/TypeScript
Familiarity with zarr for data storage
Python with pip installed
Node.js with npm installed
Overview
Creating a new view involves two main components:
Python Backend: Handles data preparation and storage in zarr format
Frontend Component: Renders the data in the browser
You can contribute your view to an existing extension like figpack_experimental
, or create your own extension. For beginners, we recommend starting with the experimental extension.
Python Backend Component
The backend component is a Python class that inherits from figpack.ExtensionView
. Here’s a simple example:
import numpy as np
import figpack
from .experimental_extension import experimental_extension
class MyView(figpack.ExtensionView):
def __init__(self, data: np.ndarray):
"""Initialize your view with data"""
super().__init__(
extension=experimental_extension,
view_type="experimental.MyView" # Must match frontend registration
)
self.data = data
def write_to_zarr_group(self, group: figpack.Group) -> None:
"""Write data to zarr group for frontend access"""
super().write_to_zarr_group(group)
# Store metadata in attributes if needed
group.attrs["some_metadata"] = "value"
# Create datasets with appropriate chunking for efficiency
group.create_dataset("data", data=self.data, chunks=True)
Key points about the Python component:
The class must inherit from
figpack.ExtensionView
The constructor accepts the data and any configuration parameters
write_to_zarr_group
is required and handles data storageThe
group
parameter is a wrapper around zarr groups (figpack uses zarr v2 format but the wrapper ensures compatibility even if zarr v3 is installed)Use attributes for metadata and datasets for the actual data
Consider chunking strategies for efficient data access
Optimizing Data Storage
For optimal performance, consider how your data will be accessed on the frontend:
Data Types: Be explicit about data types to optimize storage size
# Use appropriate data types to minimize storage data_uint8 = data.astype(np.uint8) # For 0-255 values data_float32 = data.astype(np.float32) # Instead of float64 when precision allows group.create_dataset("data", data=data_uint8)
Data Organization: Structure your data to match access patterns
# Example: Store both original and transposed data for different access patterns group.create_dataset("data", data=data) group.create_dataset("data_transpose", data=np.transpose(data))
Multi-scale Data: For large datasets, consider storing downsampled versions (see MultiChannelTimeseries.py for a detailed example)
# Store original and downsampled versions group.create_dataset("full_resolution", data=data) group.create_dataset("downsampled_2x", data=downsample(data, factor=2)) group.create_dataset("downsampled_4x", data=downsample(data, factor=4))
Compression: Zarr compresses data by default, which is especially effective for sparse data. For specialized data types, you can implement custom codecs. For example, the LossyVideo view uses MP4 compression:
# Register the MP4 codec MP4Codec.register_codec() # Use it when creating the dataset group.create_dataset("video", data=data, compressor=MP4Codec(fps=self.fps))
If using a custom codec, you’ll also need to register the corresponding decoder on the frontend (see how this is done in the experimental extension’s index.tsx).
Frontend Component
The frontend component is a React component that renders the data. It can utilize various contexts for synchronization with other views. For example, the timeseriesSelection context allows time-based views to stay synchronized.
Let’s start with a basic example:
import React from 'react';
import { ZarrGroup, FPViewContexts } from '../../figpack-interface';
interface Props {
zarrGroup: ZarrGroup;
width: number;
height: number;
contexts: FPViewContexts;
}
const MyView: React.FC<Props> = ({ zarrGroup, width, height, contexts }) => {
const [data, setData] = React.useState<any>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
const loadData = async () => {
try {
const data = await zarrGroup.getDatasetData("data", {});
setData(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load data");
} finally {
setLoading(false);
}
};
loadData();
}, [zarrGroup]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data</div>;
return (
<div style={{ width, height }}>
{/* Your visualization code here */}
</div>
);
};
export default MyView;
Key points about the frontend component:
Receives
zarrGroup
for data access, dimensions, and contexts for synchronizationUse
zarrGroup.getDatasetData()
to efficiently load data with optional slicing (details beyond scope, but see examples throughout the project)Consider implementing loading states and error handling
Use contexts for synchronization with other views
Using Contexts for Synchronization
Views can share state using contexts. For example, here’s how the FmriBold view uses the timeseriesSelection context:
import { useTimeseriesSelection } from "../../TimeseriesSelectionContext";
const MyView: React.FC<Props> = ({ zarrGroup, width, height, contexts }) => {
const { currentTime, setCurrentTime } = useTimeseriesSelection();
// Use currentTime for synchronized playback across views
const currentTimeIndex = Math.round(currentTime / temporalResolution);
// Update shared time when user interacts with this view
const handleTimeChange = (newTime: number) => {
setCurrentTime(newTime);
};
return (
// Your render code using currentTimeIndex
);
};
Real Examples from the Codebase
Simple Image View: Check out FPImage.tsx for a straightforward example of loading and displaying image data.
Complex Interactive View: The FmriBold view demonstrates interactive components with synchronized timeseries.
Video with Custom Codec: The LossyVideo view shows how to implement custom compression with MP4 encoding.
Integration Steps
1. Register the Python View
Add your view to the extension’s __init__.py
:
from .MyView import MyView
__all__ = [..., "MyView"]
2. Install and Test the Backend
Install the extension in editable mode:
cd extension_packages/figpack_experimental
pip install -e .
Create a test script to verify the backend works:
import numpy as np
from figpack_experimental.views import MyView
data = np.random.rand(100, 100)
view = MyView(data)
view.show(title="My View Test", open_in_browser=True)
Run this script and you should see a figure open in your browser, but it will display a message that the view type is not supported. This confirms the backend is working and we need to implement the frontend.
3. Register the Frontend Component
Add your component to the extension’s entry point (e.g., src/index.tsx
):
registerFPViewComponent({
name: "experimental.MyView", // Must match Python view_type
render: makeRenderFunction(MyView)
});
4. Build and Test
Build the frontend:
npm run build
Run your test script again - now you should see your custom view rendered properly.
Development Workflow
For faster development:
Run frontend in dev mode:
npm run dev
Append to browser URL:
?ext_dev=figpack-experimental:http://localhost:5174/figpack_experimental.js
Use browser dev tools and console.log() for debugging
Contributing Your View
To contribute to the experimental extension:
Create a pull request to the figpack repository
Increment the extension version
Update the changelog
Wait for the extension to be rebuilt and published
Alternatively, create your own extension using figpack_experimental as a template.
Need Help?
If you run into issues:
Check the browser console for frontend errors
Use Python debugger for backend issues
Reach out to the community for assistance
Remember: Start simple and iterate. Focus on getting a basic version working before adding complex features.