Contour
The contour aggregation returns the shape of where a bucket's data is concentrated — or of what it measured.
A convex_hull answers where does this data reach. A contour answers where is it actually concentrated, which for most questions is the one being asked. The difference is not subtle: a hull is convex, so it cannot carve out the empty middle of a ring road, cannot separate a carrier working Houston and Dallas into two territories rather than one corridor, and cannot say that half the work happens in a tenth of the ground.
GET /telematics/_search
{
"size": 0,
"aggs": {
"range": { "contour": { "field": "location" } }
}
}
Two questions
Where is this concentrated?
Give percent. The answer is percent-volume contours: the smallest region containing that share of the documents. That is what a "95% region" means in every field that uses the term — not the region covering 95% of the ground, and not the region above 95% of the peak density.
"range": { "contour": { "field": "location", "percent": [50, 95] } }
The movement-ecology literature calls these isopleths.
What is the value here?
Give value_field and levels. The answer is contours of that field's mean — isotherms, dose maps, signal strength, scan intensity.
"isotherm": {
"contour": {
"field": "station",
"value_field": "temperature_c",
"levels": [0, 5, 10, 15, 20]
}
}
The two do not mix. A proportion of documents is not a meaningful way to threshold a temperature, and an absolute density is rarely what someone asking for a home range has in mind. Asking for both, or for value_field without levels, is refused when the request is read rather than answered with something plausible.
Geographic and projected fields, one aggregation
contour works over geo_point, geo_shape, projected point and projected shape fields. They are the same question asked of a sphere and of a plane, so they are one aggregation rather than several differently-named ones.
The lattice differs, and so does how you size it:
// geo_point — cells are Web Mercator tiles, sized by zoom
"contour": { "field": "location", "precision": 14 }
// point — cells are squares in the field's own units
"contour": { "field": "position", "cell_size": 2.0 }
cell_size is required for a projected field and has no default, because a plane has no canonical extent to subdivide and 100 could be metres, feet or pixels. Each parameter is refused on the wrong field type with an explanation.
The geographic lattice is the same one geotile_grid bins to, so a contour and the heat map beneath it are computed over identical cells and cannot disagree about where a cell begins.
Shapes contour by centroid
A geo_shape field contours by centroid: one shape is one position, wherever it sits and however large it is.
"contour": { "field": "footprint", "precision": 12 }
So this answers where are these features concentrated — where the districts are, where the imagery footprints pile up. It does not answer how much ground is covered, which is a different measure.
It is the centroid because spreading would change what a level means. Contour levels are percentages of total mass; if a document's mass were spread over its extent rather than being one, a handful of large shapes would set the levels by size rather than by number. Coverage is a worthwhile second mode, not a correction to this one.
Where shapes are small next to a cell, the two measures coincide and the distinction does not arise.
A projected shape field contours the same way, by centroid, on the cartesian lattice — so it takes a cell_size rather than a precision, exactly as point does.
Parameters
| Parameter | Default | Description |
|---|---|---|
field | required | The geo_point, geo_shape, point or shape field to bin. |
percent | [50, 95] | Proportions of documents to enclose. |
levels | none | Explicit values to trace. Mutually exclusive with percent. |
value_field | none | Contour this numeric field's mean instead of document density. |
precision | 12 | Lattice resolution as a tile zoom, for geo_point and geo_shape. |
cell_size | required for point and shape | Cell width in the field's own units. |
bandwidth | ~1.5 cells | Smoothing radius, in kilometres for geographic fields. |
size | 65536 | The most cells one bucket will tally. |
format | geojson | geojson or wkb. |
The output
A GeoJSON FeatureCollection, which a mapping library's GeoJSON layer consumes directly — the response can be handed to a renderer with nothing in between reshaping it.
"range": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "MultiPolygon", "coordinates": [] },
"properties": { "value": 4.7, "percent": 95.0, "index": 0, "ramp": 0.0, "count": 39729337 }
},
{
"type": "Feature",
"geometry": { "type": "MultiPolygon", "coordinates": [] },
"properties": { "value": 31.2, "percent": 50.0, "index": 1, "ramp": 1.0, "count": 20910177 }
}
],
"properties": { "bands": 2, "cells": 18422, "resolution": 14 }
}
| Property | Description |
|---|---|
value | The threshold the band's outer edge traces — density per km², or a value of the contoured field. |
percent | The share of documents enclosed. Absent when the level was given as an explicit value. |
index | Position in the stack, 0 outermost. |
ramp | The same position normalized to 0–1. |
count | Documents inside the outer edge. |
Every property is a number, including the ones that would read more naturally as labels, because rendering accessors do arithmetic on what they are given and a string arrives as the fallback colour.
Bands, not lines
Features are isobands — the ground between one threshold and the next, with the level above cut out as holes — emitted outermost first. That matters for a colour ramp: nested fills stacked lightest-first work with opaque colours, and holes keep a translucent ramp from compounding where bands overlap.
Related
- Convex hull aggregation — how far the data reaches
- Line aggregations
- Geotile grid aggregation — the same lattice
- Geospatial