Vector Field Charts
A Vector Field Chart visualizes a 2D field of vectors — each arrow shows direction and optionally magnitude at a point in space. Common applications include fluid dynamics, electromagnetic field visualization, gradient plots in mathematics, and wind/current maps in geoscience.
SciChart.js has no dedicated vector field series; instead, vector fields are composed from FastLineSegmentRenderableSeries📘 for the vector lines, an IStrokePaletteProvider📘 for gradient coloring, and optionally FastTriangleRenderableSeries📘 for arrowheads.
The JavaScript Vector Field Example can be found in the SciChart.JS Examples Suite on GitHub, or in the live demo at scichart.com/demo.
Core Building Blocks
A vector field in SciChart.js is assembled from:
- FastLineSegmentRenderableSeries📘 — renders each vector as a line segment between a start and end point
- XyDataSeries📘 — stores alternating start/end coordinate pairs; pairs
[x₀, y₀]and[x₁, y₁]define one segment - XyxyDataSeries📘 — alternative data series with explicit
x, y, x1, y1per segment; cleaner for pre-computed data - IStrokePaletteProvider📘 with
EStrokePaletteMode.GRADIENT— applies distinct colors to each end of a segment, producing a per-vector gradient - FastTriangleRenderableSeries📘 — renders arrowheads as filled triangles positioned at each vector tip (optional)
- CentralAxesLayoutManager📘 — positions axes at the center of the chart instead of the borders, useful when the field spans negative and positive coordinates (optional)
Data Format
FastLineSegmentRenderableSeries📘 draws one independent line segment per consecutive pair of data points. The two data series options offer different ergonomics:
Using XyDataSeries (alternating pairs)
Points are appended in start, end, start, end, … order. The palette provider receives the index of each point and uses index % 2 to distinguish segment start from segment end — even indices are start points, odd indices are end points.
const dataSeries = new XyDataSeries(wasmContext);
for (let x = xMin; x <= xMax; x++) {
for (let y = yMin; y <= yMax; y++) {
// start point
dataSeries.append(x, y);
// end point — displaced by the vector at (x, y)
dataSeries.append(x + dx(x, y), y + dy(x, y));
}
}
lineSegmentSeries.dataSeries = dataSeries;
Using XyxyDataSeries (explicit start/end)
Each element in the series stores both endpoints. This avoids the alternating-index convention:
const lineSegmentPoints = [
{ x: 1, y: 1, x1: 1.4, y1: 1.3 },
{ x: 2, y: 1, x1: 2.3, y1: 1.5 },
// ...
];
const lineSegmentSeries = new FastLineSegmentRenderableSeries(wasmContext, {
dataSeries: new XyxyDataSeries(wasmContext, {
xValues: lineSegmentPoints.map(p => p.x),
yValues: lineSegmentPoints.map(p => p.y),
x1Values: lineSegmentPoints.map(p => p.x1),
y1Values: lineSegmentPoints.map(p => p.y1),
}),
strokeThickness: 2,
stroke: "cornflowerblue"
});
XyxyDataSeries cannot be used with a paletteProvider — gradient coloring per segment endpoint requires XyDataSeries with EStrokePaletteMode.GRADIENT.
Gradient Coloring
To color each vector with a gradient that runs from its tail to its tip, implement IStrokePaletteProvider📘 and set strokePaletteMode to EStrokePaletteMode.GRADIENT.
overrideStrokeArgb is called once per data point. Since points alternate start / end, even indices receive the tail color and odd indices receive the tip color:
import {
IStrokePaletteProvider,
EStrokePaletteMode,
EPaletteProviderType,
parseColorToUIntArgb,
IRenderableSeries,
TPaletteProviderDefinition
} from "scichart";
class VectorFieldPaletteProvider implements IStrokePaletteProvider {
public readonly strokePaletteMode = EStrokePaletteMode.GRADIENT;
private readonly tailColor = parseColorToUIntArgb("blue");
private readonly tipColor = parseColorToUIntArgb("orange");
public onAttached(parentSeries: IRenderableSeries): void {}
public onDetached(): void {}
public overrideStrokeArgb(xValue: number, yValue: number, index: number): number {
return index % 2 === 0 ? this.tailColor : this.tipColor;
}
public toJSON(): TPaletteProviderDefinition {
return { type: EPaletteProviderType.Custom, customType: "VectorFieldPaletteProvider" };
}
}
Attach it to the series:
const lineSegmentSeries = new FastLineSegmentRenderableSeries(wasmContext, {
strokeThickness: 3,
paletteProvider: new VectorFieldPaletteProvider()
});
Generating Vector Data from a Mathematical Field
A typical vector field is defined by a function F(x, y) = (dx, dy) that gives the displacement vector at each grid point. The demo uses the complex quadratic f(z) = z² − 4:
const multiplier = 0.01; // scales vector length
const dataSeries = new XyDataSeries(wasmContext);
for (let x = xMin; x <= xMax; x++) {
for (let y = yMin; y <= yMax; y++) {
// tail
dataSeries.append(x, y);
// tip — real and imaginary parts of z² - 4 = (x² - y² - 4) + 2xyi
dataSeries.append(
x + (x * x - y * y - 4) * multiplier,
y + (2 * x * y) * multiplier
);
}
}
lineSegmentSeries.dataSeries = dataSeries;
For a simpler trigonometric field (sine/cosine):
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
const x = i * spacing;
const y = j * spacing;
const angle = Math.sin(x) + Math.cos(y);
const dx = Math.cos(angle) * scale;
const dy = Math.sin(angle) * scale;
dataSeries.append(x, y); // tail
dataSeries.append(x + dx, y + dy); // tip
}
}
Centering the Axes
When a vector field spans both negative and positive coordinates, CentralAxesLayoutManager📘 positions the axes through the origin:
import { CentralAxesLayoutManager, EAutoRange, NumberRange } from "scichart";
sciChartSurface.layoutManager = new CentralAxesLayoutManager();
sciChartSurface.xAxes.add(new NumericAxis(wasmContext, {
visibleRange: new NumberRange(-15, 15),
autoRange: EAutoRange.Never,
drawMajorBands: false,
drawMajorGridLines: false,
drawMinorGridLines: false,
axisBorder: { color: "white", borderBottom: 1 },
zoomExtentsToInitialRange: true
}));
sciChartSurface.yAxes.add(new NumericAxis(wasmContext, {
visibleRange: new NumberRange(-10, 10),
autoRange: EAutoRange.Never,
drawMajorBands: false,
drawMajorGridLines: false,
drawMinorGridLines: false,
axisBorder: { color: "white", borderRight: 1 },
zoomExtentsToInitialRange: true
}));
Adding Arrowheads
Arrowheads make direction unambiguous and are rendered using a separate FastTriangleRenderableSeries📘. Each arrowhead is a small filled triangle placed at the tip of each vector and rotated to match its direction:
import {
FastTriangleRenderableSeries,
XyDataSeries,
ETriangleSeriesDrawMode,
parseColorToUIntArgb
} from "scichart";
// Compute arrowhead triangle vertices for each vector tip
function buildArrowheads(vectors: { x: number; y: number; dx: number; dy: number }[]) {
const xValues: number[] = [];
const yValues: number[] = [];
const size = 0.15; // arrowhead half-size
for (const { x, y, dx, dy } of vectors) {
const tipX = x + dx;
const tipY = y + dy;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
const ux = dx / len;
const uy = dy / len;
// Triangle: apex at tip, base perpendicular to vector
xValues.push(tipX, tipX - ux * size - uy * size, tipX - ux * size + uy * size);
yValues.push(tipY, tipY - uy * size + ux * size, tipY - uy * size - ux * size);
}
return { xValues, yValues };
}
const { xValues, yValues } = buildArrowheads(vectors);
const arrowSeries = new FastTriangleRenderableSeries(wasmContext, {
dataSeries: new XyDataSeries(wasmContext, { xValues, yValues }),
fill: "orange",
drawMode: ETriangleSeriesDrawMode.List
});
sciChartSurface.renderableSeries.add(arrowSeries);
Complete Example
class LineSegmentPaletteProvider implements IStrokePaletteProvider {
public readonly strokePaletteMode = EStrokePaletteMode.GRADIENT;
private readonly palettedStart = parseColorToUIntArgb("red");
private readonly palettedEnd = parseColorToUIntArgb("blue");
public onAttached(parentSeries: IRenderableSeries): void {}
public onDetached(): void {}
public overrideStrokeArgb(xValue: number, yValue: number, index: number): number {
return index % 2 === 0 ? this.palettedStart : this.palettedEnd;
}
public toJSON(): TPaletteProviderDefinition {
return { type: EPaletteProviderType.Custom, customType: "MyPaletteProvider" };
}
}
async function gradientField(divElementId) {
const { wasmContext, sciChartSurface } = await SciChartSurface.create(divElementId, {
theme: new SciChartJsNavyTheme()
});
// configure central axes
const layoutManager = new CentralAxesLayoutManager();
sciChartSurface.layoutManager = layoutManager;
const xMin = -15;
const xMax = 15;
const yMin = -10;
const yMax = 10;
const xAxis = new NumericAxis(wasmContext, {
axisBorder: { color: "white", borderBottom: 1 },
visibleRange: new NumberRange(xMin, xMax),
autoRange: EAutoRange.Never,
drawMajorBands: false,
drawMajorGridLines: false,
drawMinorGridLines: false,
zoomExtentsToInitialRange: true
});
sciChartSurface.xAxes.add(xAxis);
const yAxis = new NumericAxis(wasmContext, {
axisBorder: { color: "white", borderRight: 1 },
visibleRange: new NumberRange(yMin, yMax),
autoRange: EAutoRange.Never,
drawMajorBands: false,
drawMajorGridLines: false,
drawMinorGridLines: false,
zoomExtentsToInitialRange: true
});
sciChartSurface.yAxes.add(yAxis);
// For FastLineSegmentRenderableSeries with palette provider having SOLID palette mode the first color is used
// However for LineRendereableSeries with the same palette provider and SOLID palette the second color is used
const lineSegmentSeries = new FastLineSegmentRenderableSeries(wasmContext, {
strokeThickness: 4,
paletteProvider: new LineSegmentPaletteProvider()
});
lineSegmentSeries.rolloverModifierProps.tooltipColor = "brown";
sciChartSurface.renderableSeries.add(lineSegmentSeries);
const multiplier = 0.01;
const dataSeries = new XyDataSeries(wasmContext);
for (let x = xMin; x <= xMax; x++) {
for (let y = yMin; y <= yMax; y++) {
// start point
dataSeries.append(x, y);
// end point
const xEnd = x + (x * x - y * y - 4) * multiplier;
const yEnd = y + 2 * x * y * multiplier;
dataSeries.append(xEnd, yEnd);
}
}
lineSegmentSeries.dataSeries = dataSeries;
sciChartSurface.chartModifiers.add(new ZoomPanModifier());
sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier());
}
Chart Modifiers
The standard interaction modifiers work with vector field charts:
import {
ZoomPanModifier,
ZoomExtentsModifier,
MouseWheelZoomModifier,
CursorModifier
} from "scichart";
sciChartSurface.chartModifiers.add(
new ZoomPanModifier(),
new ZoomExtentsModifier(),
new MouseWheelZoomModifier()
);
CursorModifier📘 can display tooltips showing the start and end coordinates of a hovered vector. When using XyxyDataSeries, the tooltip data includes xValue, yValue, point2xValue, and point2yValue:
const tooltipDataTemplate = seriesInfos => {
return seriesInfos
.filter(si => si.isHit && si.isWithinDataBounds && !isNaN(si.yValue))
.map(si => `start (${si.xValue.toFixed(2)}, ${si.yValue.toFixed(2)}) → end (${si.point2xValue.toFixed(2)}, ${si.point2yValue.toFixed(2)})`);
};
sciChartSurface.chartModifiers.add(
new CursorModifier({ showTooltip: true, tooltipDataTemplate })
);
Data Series Comparison
XyDataSeries | XyxyDataSeries | |
|---|---|---|
| Data format | Alternating start, end pairs | One element per segment with x, y, x1, y1 |
| PaletteProvider support | Yes — enables gradient coloring | No |
| Tooltip coordinates | xValue, yValue per point | xValue, yValue, point2xValue, point2yValue |
| Best for | Dynamic fields with gradient coloring | Pre-computed discrete segments |