Skip to main content

The Contours Series Type

Contour maps or Contour-plots can be created using the UniformContoursRenderableSeries📘 type.

tip

The JavaScript Heatmap Chart Example can be found in the SciChart.Js Examples Suite > Contours Chart on Github, or our live demo at scichart.com/demo.

Above: The JavaScript Uniform Contours Series Chart example from the SciChart.js Demo

Create a Contours Plot​

SciChart's Contour series is an extremely fast, lightweight chart types for rendering two dimensional data as a contour plot. The UniformContoursRenderableSeries📘 type should be used in conjunction with a UniformHeatmapDataSeries📘 when you simply want to specify a Step in the X,Y direction (each cell is the same size).

To create a Javascript Contours Chart with SciChart.js, use the following code:

// Demonstrates how to create a contour plot with SciChart.js
const {
SciChartSurface,
NumericAxis,
HeatmapColorMap,
UniformHeatmapDataSeries,
UniformHeatmapRenderableSeries,
UniformContoursRenderableSeries,
SciChartJsNavyTheme
} = SciChart;
// or, for npm, import { SciChartSurface, ... } from "scichart"

// Create a SciChartSurface with X & Y Axis
const { wasmContext, sciChartSurface } = await SciChartSurface.create(divElementId, {
theme: new SciChartJsNavyTheme()
});
sciChartSurface.xAxes.add(new NumericAxis(wasmContext));
sciChartSurface.yAxes.add(new NumericAxis(wasmContext));

const WIDTH = 300;
const HEIGHT = 200;
const colorPaletteMax = 200;

// Create a Heatmap Data-series. zValues are heatValues as a 2D Array (number[][])
// Open the Codepen below to see the definition of this function
const zValues = generateExampleData(3, WIDTH, HEIGHT, colorPaletteMax);

// Create the uniform heatmap data series. Pass heatValues as number[][]
const heatmapDataSeries = new UniformHeatmapDataSeries(wasmContext, {
// 2d zValues array. Dimensions [height][width]
zValues,
// xStart, xStep, yStart, yStep defines the x,y position
xStart: 0,
xStep: 1,
yStart: 0,
yStep: 1
});

// Create a Contours RenderableSeries with the same data
const contourSeries = new UniformContoursRenderableSeries(wasmContext, {
dataSeries: heatmapDataSeries,
zMin: 20,
zMax: colorPaletteMax,
zStep: 20
});

// Add it to the scichartsurface
sciChartSurface.renderableSeries.add(contourSeries);

// Create a background heatmap series with the same data and add to the chart
const heatmapSeries = new UniformHeatmapRenderableSeries(wasmContext, {
dataSeries: heatmapDataSeries,
opacity: 0.5,
useLinearTextureFiltering: false,
// See heatmap documentation for description of how colormaps work
colorMap: new HeatmapColorMap({
minimum: 0,
maximum: colorPaletteMax,
gradientStops: [
{ offset: 1, color: "#EC0F6C" },
{ offset: 0.9, color: "#F48420" },
{ offset: 0.7, color: "#DC7969" },
{ offset: 0.5, color: "#67BDAF" },
{ offset: 0.3, color: "#50C7E0" },
{ offset: 0.2, color: "#264B93" },
{ offset: 0, color: "#14233C" }
]
})
});

// Add to the SciChartSurface
sciChartSurface.renderableSeries.add(heatmapSeries);

In the code above:

  • We create an empty 2D array number[][] using the helper function zeroArray2D. This is filled with values in the generateData function
  • A UniformHeatmapDataSeries📘 instance is created with xStart, xStep, yStart, yStep values = 0, 1, 0, 1. This means the heatmap starts at (X, Y) = (0, 0) and each cell is 1 on the axis.
  • We set the contour stroke and strokeThickness.
  • A UniformContoursRenderableSeries📘 instance is created and added to the sciChartSurface.renderableSeries📘 collection.

This results in the following output:

Updating Data in a Contour map​

The contour map is supposed to be fully dynamic, enabling real-time graphics. The Contours Series📘 however does not support append, insert, update, remove functions like other DataSeries do. You can however update the data and force a refresh simply by updating the data passed in. To do this, use the following code:

import { UniformHeatmapDataSeries, zeroArray2D } from "scichart";

const height = 10; // Set the height of the heatmap
const width = 20; // Set the width of the heatmap

// Create an empty 2D array of size height & width
const initialZValues: number[][] = zeroArray2D([height, width]);
// Create a Heatmap Data-series. Pass the heatValues as a number[][] to the UniformHeatmapDataSeries
const heatmapDataSeries = new UniformHeatmapDataSeries({
xStart: 0,
xStep: 1,
yStart: 0,
yStep: 1,
zValues: initialZValues
});

// ...
// Later, update the data
initialZValues[5][6] = 123.4;
heatmapDataSeries.notifyDataChanged() // Notify SciChart that the data has changed

// You can also load an entirely new array with the function UniformHeatmapDataSeries.setZValues
const newZValues; // type number[][]
heatmapDataSeries.setZValues(newZValues);

For more details, including a live example of how to update 2D array data for heatmaps and contours, see the Uniform Heatmap documentation - Updating Heatmaps documentation page. The mechanism for contour plots is the same.

Contours at arbitrary levels​

Using UniformContoursRenderableSeries.zLevels📘 we can specify arbitrary levels where to draw contour lines.

Use dataLabels📘 constructor option to tune the way contour labels look.

const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId);
sciChartSurface.xAxes.add(new NumericAxis(wasmContext, { visibleRange: new NumberRange(-100, 100) }));
sciChartSurface.yAxes.add(new NumericAxis(wasmContext, { visibleRange: new NumberRange(-100, 100) }));

const heatmapWidth = 200;
const heatmapHeight = 200;
const colorPaletteMin = 0; // sets Z value for 0 gradient stop
const colorPaletteMax = 10000; // sets Z value for 1 gradient stop

// Create a Heatmap Data-series. Pass heatValues as a number[][] to the UniformHeatmapDataSeries
const initialZValues: number[][] = createSeries(heatmapWidth, heatmapHeight, colorPaletteMin, colorPaletteMax);
const heatmapDataSeries = new UniformHeatmapDataSeries(wasmContext, {
xStart: 0,
xStep: 1,
yStart: 0,
yStep: 1,
zValues: initialZValues
});

// Create a Contours RenderableSeries with the same data
const contourSeries = new UniformContoursRenderableSeries(wasmContext, {
dataSeries: heatmapDataSeries,
zLevels: [1000, 4000],
strokeThickness: 2,
stroke: "white",
dataLabels: {
style: {
fontSize: 20
},
color: "red",
labelRowCount: 1,
precision: 1,
numericFormat: ENumericFormat.Decimal
}
});

// Add the contours to the chart
sciChartSurface.renderableSeries.add(contourSeries);

const gradientStops = [
{ offset: 0, color: "#00008B" },
{ offset: 0.2, color: "#6495ED" },
{ offset: 0.4, color: "#006400" },
{ offset: 0.6, color: "#7FFF00" },
{ offset: 0.8, color: "#FFFF00" },
{ offset: 1.0, color: "#FF0000" }
];

// Create a background heatmap series with the same data and add to the chart
const heatmapSeries = new UniformHeatmapRenderableSeries(wasmContext, {
dataSeries: heatmapDataSeries,
//useLinearTextureFiltering: true,
opacity: 0.8,
colorMap: new HeatmapColorMap({
minimum: colorPaletteMin,
maximum: colorPaletteMax,
gradientStops
})
});

// Add heatmap to the chart
sciChartSurface.renderableSeries.add(heatmapSeries);

Individual colouring for contour lines​

By default a contours series draws every line in one flat colour, taken from stroke (or from majorLineStyle📘 / minorLineStyle📘 when those are set). That makes it hard to tell one level from another without reading the labels.

New in v6

UniformContoursRenderableSeries.colorMapMode📘 colours each contour line individually, by its own z-value, using the series colorMap📘 — so the lines read as a legend on their own, with no background heatmap needed.

The mode is an EContourColorMapMode📘:

EContourColorMapModeEffect
SingleColor📘The default. Every line takes the flat colour from majorLineStyle / minorLineStyle. colorMap is used only to normalise the data, not to colour lines
GradientColors📘Each line's colour is interpolated from the gradient at that line's z-value, over the colorMap minimum..maximum range — so the colours follow the data, exactly as a heatmap's do
AlternateColors📘Each line takes the next gradientStops colour in turn, ignoring the offsets. With stops red, green, blue the lines cycle red, green, blue, red, ... Useful for telling adjacent levels apart when the data range is narrow

To colour each line by its z-value, set colorMapMode together with a colorMap:

// Demonstrates how to colour each contour line by its own z-value
const {
SciChartSurface,
NumericAxis,
HeatmapColorMap,
UniformHeatmapDataSeries,
UniformContoursRenderableSeries,
EContourColorMapMode,
SciChartJsNavyTheme
} = SciChart;
// or, for npm, import { SciChartSurface, ... } from "scichart"

const { wasmContext, sciChartSurface } = await SciChartSurface.create(divElementId, {
theme: new SciChartJsNavyTheme()
});
sciChartSurface.xAxes.add(new NumericAxis(wasmContext));
sciChartSurface.yAxes.add(new NumericAxis(wasmContext));

// Create a Heatmap Data-series. zValues are heatValues as a 2D array (number[][])
const heatmapDataSeries = new UniformHeatmapDataSeries(wasmContext, {
zValues: generateExampleData(300, 200),
xStart: 0,
xStep: 1,
yStart: 0,
yStep: 1
});

const contourSeries = new UniformContoursRenderableSeries(wasmContext, {
dataSeries: heatmapDataSeries,
// Draw a contour line every 20 z-values, from 20 up to the data maximum
zMin: 20,
zStep: 20,
strokeThickness: 3,
// New in v6: colour every contour line individually, by its own z-value ...
colorMapMode: EContourColorMapMode.GradientColors,
// ... reading the colour out of this colorMap, exactly as a heatmap would
colorMap: new HeatmapColorMap({
minimum: 0,
maximum: 180,
gradientStops: [
{ offset: 0, color: "#264B93" },
{ offset: 0.35, color: "#50C7E0" },
{ offset: 0.6, color: "#67BDAF" },
{ offset: 0.8, color: "#F48420" },
{ offset: 1, color: "#EC0F6C" }
]
}),
// Label each line with its z-value, so you can read which value each colour stands for
dataLabels: {
style: { fontSize: 12 },
color: "#FFFFFF",
labelRowCount: 2,
precision: 0
}
});

sciChartSurface.renderableSeries.add(contourSeries);

In the code above:

  • zMin and zStep place a contour line every 20 z-values, so there is one ring per level.
  • colorMapMode: EContourColorMapMode.GradientColors switches on per-line colouring.
  • The colorMap📘 supplies both the gradient and the range it is mapped over. A line at z = 90 sits halfway through minimum: 0..maximum: 180, so it takes the colour at gradient offset 0.5.
  • strokeThickness still sets the line width — colorMapMode only overrides the colour. Set majorLineStyle and minorLineStyle (with minorsPerMajor📘) to give majors and minors different thicknesses; both still take their colour from the colorMap.
  • dataLabels puts the z-value on each line, so you can read which value a colour stands for. See Contours at arbitrary levels above.
note

Per-line colouring needs a colorMap with gradientStops. Set colorMapMode without one and the series silently falls back to the flat single-colour path.

It also changes how the lines are drawn: SingleColor emits one draw call for all majors and one for all minors, while the per-line modes enumerate the levels and emit one draw call per level. On a chart with very many levels, prefer SingleColor. colorMapMode works with zLevels too.

Laying labels along the contour lines​

The built-in ContoursDataLabelProvider📘 places labels by scanning labelRowCount📘 evenly spaced horizontal rows across the chart and dropping an upright label wherever a row crosses a contour line. That works well when the lines run roughly horizontally and are well separated. It works less well when they are steep or tightly packed: the same line picks up one label per row, labels from neighbouring levels end up side by side, and none of them tell you which line they belong to.

Tools like matplotlib's clabel and Plotly instead put each label on its line, rotated to follow it. There is no built-in provider for that, but dataLabelProvider is an ordinary property, so you can subclass the built-in one and replace generateDataLabels with your own layout. The chart on the right below does exactly that:

const Z_LEVELS = [-60, -50, -40, -30, -20, -10, 0, 10, 20, 30];

const createChart = async (divElementId: string, dataLabelProvider: ContoursDataLabelProvider) => {
const { wasmContext, sciChartSurface } = await SciChartSurface.create(divElementId, {
theme: new SciChartJsNavyTheme()
});
sciChartSurface.xAxes.add(new NumericAxis(wasmContext));
sciChartSurface.yAxes.add(new NumericAxis(wasmContext));

const SIZE = 300;
const heatmapDataSeries = new UniformHeatmapDataSeries(wasmContext, {
zValues: generateExampleData(SIZE, SIZE),
xStart: -4,
xStep: 8 / SIZE,
yStart: -4,
yStep: 8 / SIZE
});

const contourSeries = new UniformContoursRenderableSeries(wasmContext, {
dataSeries: heatmapDataSeries,
zLevels: Z_LEVELS,
strokeThickness: 2,
stroke: "#50C7E0",
colorMap: new HeatmapColorMap({ minimum: -70, maximum: 45, gradientStops: [] })
});
// dataLabelProvider is a plain property, so a custom provider drops straight in
contourSeries.dataLabelProvider = dataLabelProvider;
sciChartSurface.renderableSeries.add(contourSeries);

sciChartSurface.chartModifiers.add(new ZoomPanModifier(), new ZoomExtentsModifier(), new MouseWheelZoomModifier());
return sciChartSurface;
};

const drawExample = async () => {
// Left: the built-in provider. Labels are upright, and land wherever its fixed rows cross a line
await createChart(
"scichart-root",
new ContoursDataLabelProvider({
style: { fontSize: 12 },
color: "#FFFFFF",
labelRowCount: 3,
precision: 0
})
);

// Right: the custom provider above. Labels follow the lines and space themselves along them
await createChart(
"scichart-root-2",
new AlongLineContoursDataLabelProvider({
style: { fontSize: 12 },
color: "#FFFFFF",
labelSpacing: 130,
rotateToLine: true,
precision: 0
})
);
};

How it works:

  • Find the lines. The contour geometry lives in a shader, so it is not readable from JavaScript. The Contour tracer tab re-derives it with marching squares: for every cell of the zValues grid, the four corners' positions relative to a level decide which cell edges the line enters and leaves by, and the crossing point on each edge is linearly interpolated. It returns loose segments rather than stitched polylines — enough to know where a line is and which way it runs, which is all a label needs.
  • Ask the series which levels it drew. getContourDrawingParams()📘 resolves zLevels, or zMin / zMax / zStep / zOffset, into the levels actually on screen, so the labels can never disagree with the lines.
  • Map grid indices to pixels. The contour surface is a texture stretched over [xMin, xMax] x [yMin, yMax] with the data samples at cell centres, hence the + 0.5. Going through the render pass's coordinate calculators keeps the labels on the lines under any zoom, pan or axis type.
  • Thin, then place. A segment is skipped if a label for the same level already sits within labelSpacing pixels — a cheap distance test done before any text measurement. What survives gets a rotationAngle from the segment's own direction, folded into a quarter turn either way so text is never upside down, and is dropped if its box leaves the chart or lands on a label already placed.
  • Override useRotation. Returning true makes the base class request the transformable font instance; without it rotationAngle is ignored.
note

This is a deliberately small recipe, not a drop-in replacement for the built-in provider. It traces the whole grid every render pass rather than caching, spaces labels by straight-line distance rather than arc length, treats major and minor lines alike, and does not handle vertical (rotated) charts. Its label positions are also not stable under zoom — because the spacing test walks segments in grid order, labels can shift or swap as the chart is zoomed. Pinning them takes anchoring each label to a position measured in zoom-invariant grid-index arc length, which is what makes a production implementation of this considerably longer.

See Also​