Ben je benieuwd naar de implementatie van deze code? Je kunt de repository vinden op github
Na het opzetten van mijn cloud-infrastructuur in Microsoft Fabric in de vorige blog, was mijn volgende uitdaging het presenteren van de golfcondities op een toegankelijke manier. In deze blog bespreek ik hoe ik de data heb ontsloten via een web-interface en welke technologische keuzes ik hierbij heb gemaakt.
Wat is het probleem dat ik moet oplossen?
Voor surfers is het cruciaal om actuele informatie over golfcondities te kunnen raadplegen voordat ze het water op gaan. Terwijl ik in mijn vorige blogs een robuuste data-infrastructuur heb opgezet, is de volgende stap deze informatie visueel en interactief toegankelijk te maken. Mijn uitdaging ligt in het ontwikkelen van een gebruiksvriendelijke interface die real-time golfcondities weergeeft op een geografische kaart.
Voor de webinterface was het mijn bedoeling om in ieder geval de golfhoogte, windrichting en windsterkte te tonen op de kaart. Aangezien ArcGIS ontzettend uitgebreid is en je veel visualisaties kunt doen, ging het in deze fase vooral om een proof of concept.
Hoe ziet het architectuurplaatje er nu uit?
In de ideale productie-omgeving zou PowerBI worden vervangen door een webpagina met ArcGIS SDK. Hoewel het niet mijn bedoeling is om deze volledig in de architectuur te integreren, wilde ik vooral onderzoeken wat er precies nodig is om JavaScript te gebruiken voor interactieve visualisaties.
Een voorbeeld van een volledige architectuur ziet er als volgt uit:

In dit scenario zou er aan de SQL server een nieuwe Fast API worden gebouwd die de data vanuit de SQL database haalt en omzet in GeoJSON-formaat. De GeoJSON kan door de ArcGIS JS SDK direct vanuit een URL worden omgezet naar een data-laag op de kaart. Een GeoJSON is een speciale soort JSON die volgens RFC 7946 is opgezet.
Voor mijn proof of concept heb ik echter een iets eenvoudigere benadering gekozen:

In deze opzet maak ik GeoJSONs direct in PySpark. Deze worden lokaal opgeslagen in een map en vervolgens via FastAPI op een URL beschikbaar gesteld. De JavaScript in de webpagina kan deze GeoJSONs inlezen en visualiseren.
Welke keuze heb ik gemaakt als het gaat om technologie? En waarom?
1. Frontend-implementatie: ArcGIS JavaScript SDK
Vergeleken met PowerBI is het wat complexer om data goed te visualiseren met de ArcGIS JS SDK. Waar bij PowerBI de datumselectie direct op alle visualisaties op een pagina werkt, moet dat in ArcGIS JS SDK specifiek worden geprogrammeerd. Dit zorgt aan de ene kant voor meer flexibiliteit in het aanpassen van mijn visualisatie, maar vergt ook meer tijd bij het opzetten.
Het grote voordeel van ArcGIS JS SDK is dat ik deze flexibiliteit direct in een webpagina (HTML-bestand) kan toepassen. Ik gebruik JavaScript om de map aan een HTML-element te koppelen, dat overal op de webpagina kan staan. Bij PowerBI, zeker in Microsoft Fabric, is de datavisualisatie eigenlijk voor intern gebruik bedoeld, en niet als product voor eindgebruikers.
2. Data-ontsluiting: FastAPI en GeoJSON
Voor de data-ontsluiting koos ik voor een combinatie van PySpark, GeoJSON en FastAPI:
- GeoJSONs genereren met PySpark: Ik heb mijn bestaande PySpark data-transformatie uitgebreid om GeoJSON-bestanden te genereren voor elke meetparameter (golfhoogte, windrichting, windsnelheid en golfperiode).
- FastAPI voor dataontsluiting: Om de GeoJSON-bestanden toegankelijk te maken vanuit mijn web-interface, heb ik een lichtgewicht FastAPI-service opgezet.
Beschrijving van de oplossing
1. Frontend-implementatie met ArcGIS JavaScript SDK

De ArcGIS JavaScript SDK is een zeer goed gedocumenteerde toolkit die ik heb gebruikt voor mijn visualisaties. De SDK biedt toegang tot talloze functies zoals kaarten, views, verschillende soorten layers, widgets en meer.
Een ArcGIS-kaart wordt in JavaScript opgebouwd uit verschillende lagen:
Map (Basislaag):
const map = new Map({
basemap: "dark-gray-vector",
layers: layers
});
MapView (Weergavelaag):
const view = new MapView({
map: map,
container: "viewDiv",
zoom: 5,
center: [4.517361000000503, 52.46373600000007]
});
Daarnaast ondersteun ik verschillende soorten layers die kunnen worden in- en uitgeschakeld via widgets. Elke laag kan ook een tijdsvak bevatten, wat cruciaal is voor mijn toepassing omdat ik historische golfcondities wil tonen.
Voor de geometrieën gebruik ik punten op de kaart, die via attributen aan tijd worden gekoppeld. Ik heb popup-templates toegevoegd voor het visualiseren van extra informatie bij het klikken op een punt.
Om de data nog duidelijker te presenteren, gebruik ik ook aanpasbare symbolen. Een punt-geometrie kan verschillende vormen aannemen, inclusief SVG-paden en tekst. De grootte en kleur van deze symbolen kan ik dynamisch aanpassen op basis van meetwaarden in de data.
2. GeoJSON-generatie en FastAPI
Voor mijn proof of concept heb ik gekozen voor een eenvoudige maar effectieve aanpak:
- GeoJSON-generatie in PySpark
- Opslag in een lokale map
- Ontsluiting via FastAPI
- Inladen en visualiseren in de webpagina via JavaScript
Een GeoJSON is een gestandaardiseerd formaat (RFC 7946) voor geografische data. Een eenvoudig GeoJSON-bestand ziet er ongeveer zo uit:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
2.950009999999986,
53.81663199999994
]
},
"properties": {
"time": 1737532800000,
"location_name": "J6 platform",
"measurement": 63.0,
"category": "Too far out"
}
}
]
}
Hierin is het type “Feature” en de “Geometry” essentieel. Je kunt elk soort geometrie inladen en de coördinaten ervan aangeven. De “properties” zijn willekeurige sleutel-waarde paren die ik later kan gebruiken voor visualisatie en filtering.
Om deze GeoJSON-bestanden te genereren, heb ik de volgende code toegevoegd aan mijn PySpark-transformatie:
import json
from pyspark.sql.functions import col
# Function to create GeoJSON
def create_geojson(df, measurement_col, file_name):
features = []
for row in df.collect():
feature = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [row["X_coordinate"], row["Y_coordinate"]]
},
"properties": {
"time": int(row["date_time"].timestamp() * 1000),
"location_name": row["location_name"],
"measurement": row[measurement_col],
"category": row["category"],
}
}
features.append(feature)
geojson = {
"type": "FeatureCollection",
"features": features
}
with open(f"../Data/{file_name}.geojson", "w") as f:
json.dump(geojson, f, indent=2)
# Create GeoJSON for each measurement type
create_geojson(location_measurement_fact.select(
col("date_time"), col("location_name"), col("X_coordinate"), col("Y_coordinate"),
col("waveperiod_measurement"), col("category")
), "waveperiod_measurement", "waveperiod_measurement")
# Similarly for other measurement types...
Deze code genereert vier verschillende GeoJSON-bestanden, één voor elke meetparameter (golfhoogte, windrichting, windsnelheid en golfperiode).
Om deze GeoJSON-bestanden beschikbaar te maken voor mijn web-interface, heb ik FastAPI gebruikt. FastAPI doet zijn naam eer aan – binnen een half uur had ik een werkend endpoint opgezet dat mijn GeoJSONs kan serveren. Het opzetten gaat als volgt:
# Installeer eerst een virtual environment
pip install virtualenv
python -m venv venv
# Activeer de omgeving
source venv/Scripts/activate
# Installeer FastAPI en Uvicorn
pip install fastapi
pip install uvicorn
# Start de FastAPI-applicatie
uvicorn main:app --reload
De `main.py` implementatie is verrassend eenvoudig:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import json
import os
# new object app from FastAPI
app = FastAPI()
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins
allow_credentials=True,
allow_methods=["*"], # Allows all methods
allow_headers=["*"], # Allows all headers
)
@app.get('/')
def index():
return 'This API returns geodata. Refer to the /docs for more information.'
@app.get('/parameter/{parameter}')
def get_parameter(parameter):
data_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Data'))
file_path = os.path.join(data_folder, f'{parameter}.geojson')
with open(file_path, 'r') as file:
data = json.load(file)
return data
3. Interactieve kaartfunctionaliteit

Een van de krachtigste aspecten van de ArcGIS JavaScript SDK is de mogelijkheid om interactieve elementen toe te voegen aan de kaart. Ik heb verschillende widgets geïmplementeerd:
Legenda:
// Add a Legend to the view
const legendExpand = new Expand({
expandTooltip: "Legend",
view: view,
content: new Legend({
view: view
}),
expanded: false
});
view.ui.add(legendExpand, "top-left");
TimeSlider (Tijdschuif):
// Create a TimeSlider
const timeSlider = new TimeSlider({
container: "timeSlider",
mode: "instant",
timeVisible: true,
stops: {
interval: {
value: 10,
unit: "minutes"
}
}
});
// Add the TimeSlider to the view
view.ui.add(timeSlider, "bottom-left");
De TimeSlider was een van de meer uitdagende elementen om te implementeren. Ik wilde dat gebruikers door verschillende tijdstippen kunnen navigeren om de veranderende golfcondities te zien. Dit heb ik gerealiseerd door de TimeSlider te koppelen aan mijn lagen met reactiveUtils:
reactiveUtils.watch(
() => timeSlider.timeExtent,
async () => {
// Get the date of the timeSlider's timeExtent and convert it to a string
const date = new Date(timeSlider.timeExtent.start).toISOString().replace("T", " ").replace("Z","");
// Update the definitionExpression of each layer
// the expression is used to filter the data based on the timeSlider
for (const layer of layers) {
layer.definitionExpression = "time = Timestamp '" + date + "'";
}
// Update the featureEffect of each layer view
for (const layerView of layerViews) {
layerView.featureEffect = {
filter: {
timeExtent: timeExtentFull,
geometry: view.extent
}
};
}
}
);
Deze code zorgt ervoor dat wanneer de gebruiker de tijdschuif verplaatst, alle lagen op de kaart worden gefilterd op het geselecteerde tijdstip. Hierdoor krijgt de gebruiker een dynamisch beeld van de veranderende golfcondities. Voor het laden van de GeoJSON-data in mijn kaart gebruik ik de GeoJSONLayer-klasse:
const layers = parameters.map((param, index) => new GeoJSONLayer({
url: `http://127.0.0.1:8000/parameter/${param}`,
title: param,
timeInfo: {
startField: "time",
timeExtent: timeExtentFull
},
renderer: renderers[index],
popupTemplate: {
title: "Surf level",
content: "{category}"
}
}));
Elke laag heeft een eigen renderer die bepaalt hoe de geometrie op de kaart wordt weergegeven. Ik heb verschillende renderers geconfigureerd voor elk type meting. Hier is bijvoorbeeld de renderer voor windsnelheid:
{
type: "simple",
field: "measurement",
symbol: {
type: "simple-marker",
path: "m 58.939631,968.39764 c -0.075,-7e-4 -0.1496,-7e-4 -0.2243,0 ...", // SVG path for wind icon
color: "#ffff00",
outline: {
color: [0, 0, 0, 0.7],
width: 0.5
},
size: 15,
xoffset: 20,
yoffset: 0
},
visualVariables: [
{
type: "size",
field: "measurement",
legendOptions: {
title: "Windspeed in m/s"
},
stops: [
{ value: 5, size: 2, label: "< 5m/s" },
{ value: 10, size: 6, label: "< 10m/s" },
{ value: 15, size: 10, label: "< 15m/s" },
{ value: 20, size: 16, label: "< 20m/s" },
{ value: 25, size: 20, label: "< 25m/s" },
{ value: 30, size: 24, label: "> 30m/s" }
]
}
]
}
Deze renderer zorgt ervoor dat windsnelheid wordt weergegeven met een aangepast pictogram, waarbij de grootte van het pictogram schaalt met de windsnelheid.
Welke work-arounds?
De grootste uitdaging tijdens de implementatie was het koppelen van de TimeSlider aan de geometrieën in de verschillende lagen. De documentatie hierover was niet altijd even duidelijk. Door het bestuderen van verschillende voorbeelden en experimenteren met verschillende benaderingen, heb ik uiteindelijk een werkende oplossing gevonden.
Een belangrijk inzicht was dat de TimeSlider hetzelfde tijdsvak moet hebben als de geometrieën in de lagen. Daarnaast moet je de juiste modus voor de TimeSlider instellen, afhankelijk van of je een enkel moment of een tijdsvak wilt selecteren.
Het updaten van de lagen op basis van de TimeSlider-positie was ook een uitdaging. Ik heb dit opgelost door de tijd van de TimeSlider om te zetten naar een expressie die ik vervolgens toepas op de `definitionExpression`-eigenschap van elke laag.
Wat is de conclusie?
ArcGIS JavaScript SDK is een krachtige toolkit voor geografische visualisaties. Hoewel de leercurve iets steiler is dan bij PowerBI, biedt het aanzienlijk meer flexibiliteit en mogelijkheden voor interactieve webvisualisaties.
De belangrijkste inzichten uit dit project:
- Complexiteit versus flexibiliteit: ArcGIS JavaScript SDK vereist meer programmeerwerk dan PowerBI, maar biedt veel meer aanpassingsmogelijkheden voor interactieve visualisaties.
- Efficiëntie van moderne tools: Met FastAPI is het opzetten van een REST API voor GeoJSON-bestanden opmerkelijk snel gerealiseerd, zelfs voor een proof of concept.
- Doelgroepgerichte visualisatie: Een webinterface met ArcGIS JavaScript is beter geschikt voor publieke toepassingen, terwijl PowerBI zijn kracht behoudt voor interne dashboards.
In deze proof of concept heb ik laten zien dat het mogelijk is om een interactieve kaartvisualisatie te maken die golfcondities weergeeft door middel van de ArcGIS JavaScript SDK en FastAPI. De volgende stap zou zijn om deze oplossing te integreren in een productieomgeving.
In het volgende deel van deze blogreeks ga ik dieper in op een alternatief voor Microsoft Fabric met FME, en hoe dit dataplatform zich verhoudt tot mijn huidige oplossing. Daarnaast zal ik ook onderzoeken hoe ik machine learning kan toepassen om automatisch de geschiktheid van surflocaties te bepalen op basis van de huidige condities.
Mis het niet! 😉
Eerdere blogs in deze reeks: