Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
fields = new Map([
["left to right", (x, y) => [x, 0]], // Horizontal flow from left to right.
["diagonal", (x, y) => [x, x]], // Diagonal flow from top-left to bottom-right.
["circle", (x, y) => [y - 0.5, 0.5 - x]], // Circular flow.
["repel", (x, y) => [x - 0.5, y - 0.5]], // Repelling flow from the center.
])
Insert cell
Insert cell
exampleVector = fields.get("left to right")(0.5, 0.5) // Sample the field.
Insert cell
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
Insert cell
function dirToColor(dir) {
// Normalize the vector (value range from -1 to 1).
const normal = getNormalized(dir);
// Map values to be between 0 and 1.
normal[0] = (normal[0] + 1) * 0.5;
normal[1] = (normal[1] + 1) * 0.5;
// Convert to array of color values.
return [ Math.floor(normal[0] * 255), Math.floor(normal[1] * 255), 0, 255 ];
}
Insert cell
mutable animatedVector = [ 0, 0 ]; // Example vector (same as in the animation above).
Insert cell
animatedVectorColor = dirToColor(animatedVector) // Animated vector converted to color.
Insert cell
Insert cell
async function createImage(ctx, width, height, field, alphaScale = 1.0) {
// Create the image data.
let image = ctx.createImageData(width, height);

// Loop through the image pixels.
for (let y = 0, i = 0; y < height; y++) {
// Convert pixel-y to field-y (value between 0 and 1).
const fy = y / (height - 1);
for (let x = 0; x < width; x++, i += 4) {
// Convert pixel-x to field-x (value between 0 and 1).
const fx = x / (width - 1);
// Retrieve the vector from the field for the current pixel.
const dir = field(fx, fy);
// Convert the vector to a color.
const color = dirToColor(dir);

// Assign the color values to the pixel color.
image.data[i] = color[0]; // Red
image.data[i + 1] = color[1]; // Green
image.data[i + 2] = color[2]; // Blue
image.data[i + 3] = color[3] * alphaScale; // Alpha
}
}

// Create and return a bitmap image from the image data.
return createImageBitmap(image);
}
Insert cell
Insert cell
{
// Create a canvas.
const size = Math.min(width, 450);
const ctx = DOM.context2d(size, size);
ctx.canvas.style.border = "solid 1px black";
// Get a field.
const field = fields.get("circle");
// Create and draw the image.
const image = await createImage(ctx, size, size, field);
ctx.drawImage(image, 0, 0, size, size);

// Return the canvas.
return ctx.canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawArrow(ctx, cx, cy, dir, size, lineWidth = 1, strokeColor = "#000") {
// Calculate arrow properties.
const mag = getMagnitute(dir);
const normal = getNormalized(dir);
const angle = Math.atan2(dir[1], dir[0]);

// Use the vector magnitude to scale the length of the arrow.
const length = Math.max(1, size * mag);

// Calculate start and end position of arrow.
const x0 = cx - normal[0] * length;
const y0 = cy - normal[1] * length;
const x1 = cx + normal[0] * length;
const y1 = cy + normal[1] * length;

// Set the render properties.
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.beginPath();

// Set the center line of the arrow.
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);

// Set the lines of the arrow head.
ctx.moveTo(x1, y1);
ctx.lineTo(x1 - length * Math.cos(angle - Math.PI / 6), y1 - length * Math.sin(angle - Math.PI / 6));
ctx.moveTo(x1, y1);
ctx.lineTo(x1 - length * Math.cos(angle + Math.PI / 6), y1 - length * Math.sin(angle + Math.PI / 6));

// Draw the arrow.
ctx.stroke();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function createParticle(x, y, size = 3, time = 20, color = "#a6cee3") {
// Return a particle object that stores the particle positions and its properties.
return { x: x, y: y, size: size, color: color, time: time }
}
Insert cell
Insert cell
function createParticles(count = particleProps.count, size = particleProps.size, time = particleProps.lifetime) {
// Create an array of particles with random positions.
let particles = [];
for (let i = 0; i < count; ++i) {
particles.push(createParticle(Math.random(), Math.random(), size, time));
}
return particles;
}
Insert cell
Insert cell
function updateParticle(particle, field, dt = 1, speed = 0.01) {
// Get the particle's x and y coordinate.
let { x, y } = particle;
// Lower the particle's lifetime based on the elapsed time.
particle.time -= dt;
// Check if the particle is inside boundaries and alive.
if (x >= 0 && x <= 1 && y >= 0 && y <= 1 && particle.time > 0) {
// Sample the field at the current particle position.
const dir = field(x, y);
// Move the particle based on the sampled field vector, scaled by the elapsed time and velocity factor.
particle.x = x + dir[0] * dt * speed;
particle.y = y + dir[1] * dt * speed;
} else {
// Reset the particle (outside boundary or beyond lifetime).
resetParticle(particle);
}
}
Insert cell
Insert cell
function resetParticle(particle, time = particleProps.lifetime) {
// Reset the particle position.
particle.x = Math.random();
particle.y = Math.random();
// Reset the particle lifetime.
particle.time = time;
}
Insert cell
Insert cell
function drawParticle(ctx, particle) {
// Get the width and height of the canvas.
const width = ctx.canvas.width / window.devicePixelRatio;
const height = ctx.canvas.height / window.devicePixelRatio;
// Construct a circle to represent the particle.
ctx.beginPath();
ctx.arc(
particle.x * width, // Convert x to canvas coordinates.
particle.y * height, // Convert y to canvas coordinates.
particle.size,
0,
2 * Math.PI
);

// Fill the circle.
ctx.fillStyle = particle.color;
ctx.fill();

// Draw the circle outline.
ctx.strokeStyle = "#333333";
ctx.stroke();
}
Insert cell
Insert cell
{
await visibility();
// Create a canvas.
const size = Math.min(width, 450);
const ctx = DOM.context2d(size, size);
ctx.canvas.style.border = "solid 1px black";

// Get a field and create a background image.
const field = fields.get("repel");
const image = await createImage(ctx, size, size, field);

// Create the particles.
const particles = createParticles(15);
// Animation loop.
let time = performance.now();
while (true) {
// Clear the canvas.
ctx.clearRect(0, 0, size, size);

// Draw the background image.
ctx.drawImage(image, 0, 0, size, size);

// Calculate the time delta.
const now = performance.now();
const dt = (now - time) / 1000;
time = now;

// Update and draw the particles.
particles.forEach((particle) => {
updateParticle(particle, field, dt, 100 / size);
drawParticle(ctx, particle);
});
// Return the canvas.
yield ctx.canvas;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
function drawArrowGrid(ctx, field, gridSize, arrowSize = 10) {
// Get width and height of canvas.
const width = ctx.canvas.width / window.devicePixelRatio;
const height = ctx.canvas.height / window.devicePixelRatio;
// Calculate width and height of grid cell.
const xStep = width / gridSize;
const yStep = height / gridSize;

// Loop through the cell in the grid.
for (let y = yStep * 0.5; y < height; y += yStep) {
// Calculate the field y-coordinate.
let fy = y / (height - 1);
for (let x = xStep * 0.5; x < width; x += xStep) {
// Calculate the field x-coordinate.
let fx = x / (width - 1);
// Sample the field for the center of the grid cell.
let dir = field(fx, fy);

// Draw an arrow for the grid.
drawArrow(ctx, x, y, dir, arrowSize);
}
}
}
Insert cell
Insert cell
{
// Create a canvas.
const size = Math.min(width, 450);
const ctx = DOM.context2d(size, size);

// Render properties.
ctx.canvas.style.border = "solid 1px black";
// Shadow color and blur to highlight arrows against a dark background.
ctx.shadowColor = "#fff";
ctx.shadowBlur = 5;

// Get a field and create a background image.
const field = fields.get("circle");
const image = await createImage(ctx, size, size, field);
// Clear the canvas.
ctx.clearRect(0, 0, size, size);

// Draw the background image.
ctx.drawImage(image, 0, 0, size, size);

// Draw grid of arrows.
drawArrowGrid(ctx, field, 10, 15);
// Return the canvas.
yield ctx.canvas;
}
Insert cell
Insert cell
Insert cell
function reduceOpacity(ctx, fadeOut) {
// Get the current image content of the canvas.
let image = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);

// Loop through all the pixels in the image.
for (let i = 3; i < image.data.length; i += 4) {
// Reduce the value of the alpha channel.
image.data[i] = Math.max(0, image.data[i] - fadeOut); // Make sure that alpha does not become negative.
}

// Put the modified image content back into the canvas.
ctx.putImageData(image, 0, 0);
}
Insert cell
Insert cell
{
await visibility();
// Create the main canvas which will be on screen.
const size = Math.min(width, 450);
const ctx = DOM.context2d(size, size);
ctx.canvas.style.border = "solid 1px black";

// Create a secondary offscreen canvas for rendering effects.
const effectCtx = DOM.context2d(size, size);
// Get a field.
const field = fields.get("circle");

// Create the particles.
const particles = createParticles(15);

// Animation loop.
let time = performance.now();
while (true) {
// Calculate the time delta.
const now = performance.now();
const dt = (now - time) / 1000;
time = now;

// Instead of clearing the canvas, we reduce the opacity of the existing content.
// The fade out rate has to be an integer and greater than one to have an effect
// because the alpha channel of the image data takes 8-bit integer values.
const fadeOut = Math.max(1, Math.floor(100 * dt));
reduceOpacity(effectCtx, fadeOut);
// Update and draw the particles.
particles.forEach((particle) => {
updateParticle(particle, field, dt, 100 / size);
drawParticle(effectCtx, particle);
});

// Clear the main canvas.
ctx.clearRect(0, 0, size, size);
// Render the effect canvas.
ctx.drawImage(effectCtx.canvas, 0, 0, size, size);
// Return the canvas.
yield ctx.canvas;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
additionalFields = new Map([
["saddle", (x, y) => [x - 0.5, -y + 0.5]],
["attract", (x, y) => [0.5 - x, 0.5 - y]],
["swirl", (x, y) => {
x = (x - 0.5) * 4 * Math.PI;
y = (y - 0.5) * 4 * Math.PI;
return [Math.sin(x + y), Math.cos(x - y)];
}],
["hilly bowl", (x, y) => {
x = (x - 0.5) * 4 * Math.PI;
y = (y - 0.5) * 4 * Math.PI;
return [0.5, Math.sin(x * x + y * y)];
}],
["vortex", (x, y) => {
x = (x - 0.5) * 10;
y = (y - 0.5) * 10;

const radius = 100.0, pull = 0.05;
let sqSum = Math.max(x * x + y * y, 0.00001);
let divisor = !sqSum ? 1 : 1 / Math.sqrt(sqSum);
let factor = Math.exp(-sqSum / radius);
let u = y * factor * divisor - pull * x;
let v = -x * factor * divisor - pull * y;
return [u, v];
}],
])
Insert cell
Insert cell
additionalFields.forEach((value, key) => fields.set(key, value) );
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
combinedField0 = (x, y) => addVector(fields.get("circle")(x, y), fields.get("repel")(x, y))
Insert cell
Insert cell
combinedField1 = (x, y) => {
const v0 = fields.get("left to right")(x, y);
const v1 = fields.get("attract")(x, y);
const v2 = fields.get("diagonal")(x, y);

return scaleVector(addVector(addVector(v0, v1), v2), 0.3);
}
Insert cell
Insert cell
combinedField2 = (x, y) => {
const v0 = scaleVector(fields.get("left to right")(x, y), 3);
const v1 = fields.get("swirl")(x, y);
return scaleVector(addVector(v0, v1), 0.25);
}
Insert cell
Insert cell
Insert cell
Insert cell
{
await visibility();
// Create a canvas.
const size = Math.min(width, 350);
const ctx = DOM.context2d(size, size);

// Render properties.
ctx.canvas.style.border = "solid 1px black";
ctx.shadowColor = "#fff";
ctx.shadowBlur = 5;

// Animation loop.
while (true) {
// Calculate the interpolation factor as a function of time.
const t = Math.sin(performance.now() / 1000);

// Define a field function.
const field = (x, y) => {
const v0 = fields.get("circle")(x, y);
const v1 = fields.get("saddle")(x, y);
return interpolateVector(v0, v1, t);
};
// Get a field and create a background image.
const image = await createImage(ctx, 0.5 * size, 0.5 * size, field);
// Clear the canvas.
ctx.clearRect(0, 0, size, size);
// Draw the background image.
ctx.drawImage(image, 0, 0, size, size);
// Draw grid of arrows.
drawArrowGrid(ctx, field, 10);
// Return the canvas.
yield ctx.canvas;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
particles = createParticles(particleProps.count, particleProps.size, particleProps.lifetime)
Insert cell
Insert cell
flowVis = {
// Create main onscreen canvas.
const size = Math.min(width, 350);
const ctx = DOM.context2d(size, size);

// Render properties.
ctx.canvas.style.border = "solid 1px black";
ctx.shadowColor = "#fff";
ctx.shadowBlur = 5;

// Create a secondary offscreen canvas for particle effects.
const particleCtx = DOM.context2d(size, size);

// Animation loop.
let time = performance.now();
while (true) {
// Calculate the time delta.
const now = performance.now();
const dt = (now - time) / 1000;
time = now;

// Calculate the field interpolation factor as a function of time.
const t = Math.sin(0.5 * now / 1000);
// Define a field function.
const field = (x, y) => {
const v0 = fieldProps.fieldA(x, y);
const v1 = fieldProps.fieldB(x, y);
return interpolateVector(v0, v1, fieldProps.interpolate === "yes" ? t : 0);
};

// Reduce the opacity of previous content in particle canvas.
const fadeOut = Math.max(1, particleProps.fadeOut * Math.floor(100 * dt));
reduceOpacity(particleCtx, fadeOut);
// Update and draw the particles.
particles.forEach((particle) => {
updateParticle(particle, field, dt, particleProps.speed * 100 / size);
drawParticle(particleCtx, particle);
});
// Create a background image.
const image = await createImage(ctx, size * 0.5, size * 0.5, field, fieldProps.alphaScale);
// Clear the canvas.
ctx.clearRect(0, 0, size, size);
// Draw the background image.
ctx.drawImage(image, 0, 0, size, size);
// Draw grid of arrows.
ctx.shadowBlur = 5;
drawArrowGrid(ctx, field, arrowProps.gridSize, arrowProps.size);
ctx.shadowBlur = 0;
// Draw the particle canvas.
ctx.drawImage(particleCtx.canvas, 0, 0, size, size);
// Return the canvas.
yield ctx.canvas;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more