How Sylvera uses Fused to prototype and power DeckGL applications
TL;DR Sylvera quickly builds and tests new app features by serving data to DeckGL applications using Fused HTTP endpoints.
At its core, Sylvera rates carbon projects. Our ratings are powered by several earth observation and geospatial analysis data products. From climate risk data, and deforestation indicators, to biomass-predicting ML models, a wealth of data goes into generating a single-letter rating.
The problemβ
With such a wealth of information, it's challenging to determine how best to present insights to the client. On one hand, we want the client to be able to see the data that tell the story of the project's rating. On the other, we don't want to overwhelm them with fun interactive visualizations that leave them without a clear takeaway.
This is why we decided to organize an internal hackathon, aimed at discovering what the modern geospatial data stack can do when it comes to enabling user experience with geospatial data.
The solutionβ
To enable quick iteration, selecting the right technology stack was crucial. Our data lives in Zarr format in Arraylake, a data lake platform for managing array data.
We explored the current landscape and considered several options: Should we serve pre-computed visualizations through xpublish or TiTiler? Create interactive dashboards through Felt, or build them in Mapbox?
Ultimately, we chose the most agile and powerful stack: an interactive React + deck.gl application powered entirely by Fused.
We created a React dashboard, where each element renders the output of Fused UDFs. This required an entire DAG of UDFs.
The core element is the Data UDF, which fetches raw data for a given area of interest. The subsequent UDFs then process this data for the application.
In the example below, The Visualisation and Data Overlay UDFs prepare a web map. The Histogram UDF uses the hammer-and-nails pattern to get the distribution for each year. The Timeseries UDF computes the average per year. This setup is highly modular and makes it straightforward to present the same data in a myriad of different ways.
This is the custom function to create a layer from a Fused HTTP endpoint called as a File which returns a .png
.
const createFusedFileLayer = (
layerId: string,
fusedId: string,
bounds: [number, number, number, number],
year: number
) => {
const key = `${layerId}-${year}`;
const param = `year=${year}`;
const imageUrl = `https://www.fused.io/server/v1/realtime-shared/${fusedId}/run/file?dtype_out_raster=png&${param}`;
const layer = createBitmapLayer(key, imageUrl, bounds);
return layer;
};
View the full code:
import { createRoot } from "react-dom/client";
import DeckGL from "@deck.gl/react";
import { MapView } from "@deck.gl/core";
import { BitmapLayer } from "@deck.gl/layers";
import { TileLayer } from "@deck.gl/geo-layers";
const BOUNDS_AFRICA: [number, number, number, number] = [
-25.35, -46.95, 51.35, 37.35,
];
const UDF_H_AOI_FILE_CALL = "FUSED_UDF";
function createTileLayer(id: string, data: string[]): TileLayer<ImageBitmap> {
return new TileLayer<ImageBitmap>({
id: id,
data: data,
maxRequests: 20,
maxCacheSize: 200,
pickable: true,
highlightColor: [60, 60, 60, 40],
minZoom: 0,
maxZoom: 19,
tileSize: 256,
zoomOffset: devicePixelRatio === 1 ? -1 : 0,
renderSubLayers: (props) => {
const [[west, south], [east, north]] = props.tile.boundingBox;
const { data, ...otherProps } = props;
return [
new BitmapLayer(otherProps, {
image: data,
bounds: [west, south, east, north],
}),
];
},
});
}
function createBitmapLayer(
id: string,
image: string,
bounds: [number, number, number, number]
) {
return new BitmapLayer({
id: id,
image: image,
bounds: bounds,
pickable: true,
highlightColor: [60, 60, 60, 40],
minZoom: 0,
maxZoom: 19,
});
}
const createFusedFileLayer = (
layerId: string,
fusedId: string,
bounds: [number, number, number, number],
year: number
) => {
const key = `${layerId}-${year}`;
const param = `year=${year}`;
const imageUrl = `https://www.fused.io/server/v1/realtime-shared/${fusedId}/run/file?dtype_out_raster=png&dtype_out_vector=csv&${param}`;
const layer = createBitmapLayer(key, imageUrl, bounds);
return layer;
};
const createBasemapLayer = () => {
return createTileLayer("basemap", [
"https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
]);
};
export default function App() {
const bounds = BOUNDS_AFRICA;
const year = 2000;
const basemap = createBasemapLayer();
const fused = createFusedFileLayer(
"fused",
UDF_H_AOI_FILE_CALL,
bounds,
year
);
return (
<DeckGL
layers={[basemap, fused]}
views={new MapView({ repeat: true })}
initialViewState={{ latitude: 0.34211, longitude: 15.151583, zoom: 2 }}
controller={true}
></DeckGL>
);
}
export function renderToDOM(container: HTMLDivElement) {
createRoot(container).render(<App />);
}
Conclusion and future workβ
The application we built isn't yet fully featured to be put in front of users β but that's the point. We were not aiming for a finished product yet. Instead, we achieved rapid iteration that enabled us to gather relevant stakeholder feedback.
The speed we could reach wouldn't have been possible without Fused's development platform. Fused unifies three traditionally separate stages β prototyping, scaling, and visualizationβinto a single seamless solution. Thanks to this, Fused was an indispensable tool for product iteration.
In the future, we would like to explore the coming integration with Zarr stores. Being able to not only visualize the results but also to immediately persist them into a Zarr store will be a game-changing capability for anyone who uses Zarr as the persistence layer.