Add animated streamlines for vector fields#194
Conversation
|
Btw. I tried it with @leifdenby's data, e.g. this one: https://harmonie-zarr.s3.amazonaws.com/dini/control/2026-07-23T030000Z/single_levels.zarr Which should have u10 and v10. Why doesn't it work for those? Another thing here is, that Leif Denby's Fork creates an artificial variable |
I think for my initial azimutal branch, I got it somewhat solved by not projecting on a flat 2d surface, but on a very shallow cone, pointing towards the camera instead. This brings those lines to the back, and if the cone is filled with black, you don't see the lines. |
|
|
Ok, I tried it again right now, more points about the UX:
|
|
The latest commits should cover all of the discussed points above |
|
I have a Gaussian Reduced dataset (locally unfortunetaly, @wachsylon where is the online source of Looks like this:
The color underneath is the |
|
I asked Codex about the issue with the missing streamlines between -90 and 0: The local zarr data is complete. For time index 0, avg_10u and avg_10v both have 654400/654400 finite values. The dataset stores longitude as 0..359.777, so the displayed -90..0 region corresponds to raw 270..360, and that band also has finite wind data. Here is the code it created. Please don't add the thousandth Version of "normalizeLongitude". They are everywhere in Gridlook.... // vectorField.ts
function normalizeLongitude180(longitude: number) {
return ((((longitude + 180) % 360) + 360) % 360) - 180;
}
function normalizeLongitudes180(longitudes: Float32Array) {
return Float32Array.from(longitudes, normalizeLongitude180);
}
function normalizeGlobalLongitudes(longitudes: Float32Array) {
const bounds = finiteBounds(longitudes);
const isGlobal = bounds.maximum - bounds.minimum > 300;
return {
isGlobal,
longitudes: isGlobal ? normalizeLongitudes180(longitudes) : longitudes,
};
}
export class IrregularVectorField implements TStreamlineVectorField {
readonly isGlobal: boolean;
readonly latitudeMin: number;
readonly latitudeMax: number;
readonly longitudeMin: number;
readonly longitudeMax: number;
readonly referenceSpeed: number;
private readonly index: KDBush;
private readonly regularField: RegularVectorField | undefined;
private readonly latitudes: Float32Array;
private readonly longitudes: Float32Array;
private readonly uData: Float32Array;
private readonly vData: Float32Array;
private readonly interpolationRadiusKm: number;
constructor(
latitudes: Float32Array,
longitudes: Float32Array,
uData: Float32Array,
vData: Float32Array
) {
if (
latitudes.length === 0 ||
longitudes.length !== latitudes.length ||
uData.length !== latitudes.length ||
vData.length !== latitudes.length
) {
throw new Error("Vector components do not match the unstructured grid");
}
const samples = filterFiniteVectorSamples(
latitudes,
longitudes,
uData,
vData
);
if (samples.latitudes.length === 0) {
throw new Error("Vector field has no finite samples");
}
this.latitudes = samples.latitudes;
const normalizedLongitudes = normalizeGlobalLongitudes(samples.longitudes);
this.isGlobal = normalizedLongitudes.isGlobal;
this.longitudes = normalizedLongitudes.longitudes;
this.uData = samples.uData;
this.vData = samples.vData;
const latitudeBounds = finiteBounds(this.latitudes);
const longitudeBounds = finiteBounds(this.longitudes);
this.latitudeMin = Math.max(-89.5, latitudeBounds.minimum);
this.latitudeMax = Math.min(89.5, latitudeBounds.maximum);
this.longitudeMin = longitudeBounds.minimum;
this.longitudeMax = longitudeBounds.maximum;
this.interpolationRadiusKm = interpolationRadiusKm(
this.latitudes.length,
this.latitudeMin,
this.latitudeMax,
this.longitudeMin,
this.longitudeMax
);
this.referenceSpeed = calculateReferenceSpeed(this.uData, this.vData);
this.index = new KDBush(this.latitudes.length);
for (let i = 0; i < this.latitudes.length; i++) {
this.index.add(this.longitudes[i], this.latitudes[i]);
}
this.index.finish();
this.regularField = this.createRegularField();
}
....
// vectorField.test.ts
it("samples global unstructured fields stored with 0..360 longitudes", () => {
const latitudes: number[] = [];
const longitudes: number[] = [];
const uData: number[] = [];
const vData: number[] = [];
for (const latitude of [-1, 0, 1]) {
for (let longitude = 0; longitude < 360; longitude++) {
latitudes.push(latitude);
longitudes.push(longitude);
uData.push(longitude);
vData.push(0);
}
}
const field = new IrregularVectorField(
Float32Array.from(latitudes),
Float32Array.from(longitudes),
Float32Array.from(uData),
Float32Array.from(vData)
);
expect(field.isGlobal).toBe(true);
expect(field.sample(0, -45)?.u).toBeCloseTo(315);
expect(field.sample(0, -90)?.u).toBeCloseTo(270);
}); |


Adds optional animated streamlines for vector datasets.
Gridlook detects
u/v,ua/va, anduas/vaspairs automatically. Other vector components, such as ocean currents, can be selected manually in the Flow panel.The streamlines use cached RK4/3 trajectories with fading, speed-dependent traces. They work across the supported grid types, including HEALPix, and are disabled by default because building the initial trajectory cache can take a moment.
Tested with the ICON HEALPix
uas/vasdataset. Unit tests, lint, typecheck, and the production build pass.Screencast.from.2026-07-23.03-16-20.webm