Sizing & Scaling

How iXMaps sizes chart symbols: sizing principles, the three sizing levers — normalSizeScale, normalsizevalue, scale — plus the sizepow dimension and zoom anchoring.

iXMaps provides several complementary mechanisms for controlling how symbols and charts are sized. Understanding how they interact helps you get the right visual density at every zoom level.


Sizing principles

Before the individual parameters, four principles explain why the sizing system is built the way it is. Keeping them apart prevents the most common sizing mistakes.

1 · Size contrast within a theme

Inside one theme, the ratio between symbol sizes encodes the ratio between data values. How strongly that contrast appears is controlled by sizepow — whether the value is carried by the symbol’s width, area (default), or volume. This is a property of the theme’s internal comparison and has nothing to do with how big the theme looks overall.

2 · Same-size rule between themes

When two themes should be comparable — the same indicator for two years, two indicators on the same map, or a swappable-theme setup — the same data value must render at the same symbol size in every theme. That is achieved by giving all comparable themes the same explicit normalsizevalue (and the same sizepow). If each theme is left to size itself, equal values will render at different sizes and the visual comparison silently lies.

3 · Zoom adaptation — one setting for the whole map

How symbols grow and shrink while zooming is a map-level decision, made once in .options() (objectscaling: "dynamic" + normalSizeScale), and it applies equally to all themes. This keeps the size relations between themes stable at every zoom level. Never try to emulate zoom behaviour per theme — that breaks the same-size rule the moment the user zooms.

4 · Reserve scale for the runtime API

scale is a bare multiplier with no data meaning. Using it at design time to “fix” a theme’s size hides the real reference (normalsizevalue) and breaks comparability between themes. Leave it at 1 in layer definitions and treat it as the lever for interactive, runtime adjustments:

myMap.then(api => api.changeThemeStyle("layerName", "scale:1.2", "factor"));

(Exception: compensating a purely visual effect — e.g. scale: 0.32 to offset a |GLOW halo — is legitimate, because it corrects appearance, not data mapping.)

Defaults and automatic sizing

If a theme defines no normalsizevalue, iXMaps sizes it automatically: the reference becomes the maximum value of the theme’s currently loaded data. The remaining defaults are sizepow: 2 (area-proportional) and scale: 1.

Automatic sizing is convenient for a first look at a single layer, but it makes the size mapping data-dependent, with two consequences:

  • Filtering changes symbol sizes. After a filter, the data maximum may change — and with it the reference, so all symbols rescale. The same feature with the same value can render larger after filtering, which reads as a change in the data when only the selection changed.
  • Themes are not comparable. Each auto-sized theme normalizes against its own maximum, so a value of 1000 renders at a different size in every theme.

For any map with filters, swappable themes, or side-by-side comparison, set normalsizevalue explicitly — it pins the value→size mapping independently of what data happens to be loaded.


The three sizing levers

Lever Scope Where Effect
normalSizeScale Map .options() Reference map scale denominator — sets the zoom level at which symbols appear at their “nominal” size
normalsizevalue Layer .style() Reference data value that maps to nominal size — higher value = smaller symbols
scale Layer .style() Direct multiplier applied on top of everything else — reserve for runtime adjustments (principle 4)

They multiply together. A symbol’s screen size is approximately:

screen size ∝ (data value / normalsizevalue) * scale * f(currentZoom / normalSizeScale)

normalSizeScale — the zoom anchor

Set in .options(). Tells iXMaps: “at this map scale denominator, render symbols at their nominal size.”

Zoom level Approximate normalSizeScale
4 (continent) "30000000"
5 (large country) "15000000"
6 (country, e.g. Italy) "8000000"
8 (region) "2000000"
10 (province) "500000"
12 (city) "100000"
14 (district) "25000"
.options({
    objectscaling:   "dynamic",
    normalSizeScale: "8000000",   // zoom 6 = nominal size
})
WarningNever set normalSizeScale below ~10 000

Very small values (e.g. "1") invert the scaling and produce wildly oversized or invisible symbols at all zoom levels. Always use a geographically meaningful scale denominator.


normalsizevalue — the data anchor

Set in .style(). Declares: “a data value equal to normalsizevalue renders at the nominal symbol size.”

Higher value → smaller symbols (because most real data values fall below it).

// If your data has values up to 10 000 000:
.style({ normalsizevalue: "5000000" })  // median-ish value → medium bubbles

// If your values are small (0–100):
.style({ normalsizevalue: "50" })       // smaller reference → larger bubbles

scale — direct multiplier

The simplest lever: a bare multiplier applied after everything else. Per principle 4, keep it at 1 in layer definitions and size themes via normalsizevalue; use scale for runtime adjustments and for compensating visual effects:

.style({ scale: 0.32 })  // legitimate: offset the |GLOW halo's apparent size

// runtime — interactive size adjustment:
myMap.then(api => api.changeThemeStyle("layerName", "scale:0.9", "factor"));

sizepow — which symbol dimension carries the value

Purpose

A numeric data value has to become a symbol size on screen — but “size” is ambiguous: it could mean the symbol’s width (linear), its area (2D), or its apparent volume (3D). sizepow resolves that ambiguity. It declares the geometric dimension of the symbol that represents the data value, and thereby controls how strongly size differences between small and large values appear.

The cartographic convention for 2D symbols (circles, squares) is area-proportional: a value twice as large gets a symbol with twice the area — not twice the diameter, which would quadruple the area and grossly exaggerate differences. That convention is sizepow: 2, and it is the iXMaps default.

Definition

The symbol’s linear dimension (radius, side, height) is proportional to the value raised to 1/sizepow:

symbolSize = referenceSize × (value ^ (1/sizepow)) / (referenceValue ^ (1/sizepow))

where referenceValue is normalsizevalue if set, otherwise the layer’s maximum value. Read sizepow as a dimension: with sizepow: 2 the value is carried by the symbol’s surface (radius grows with the square root), with sizepow: 3 by its volume, with sizepow: 1 by its length.

Possibilities

Set it directly in .style() — any positive number works:

.style({ sizepow: 1 })    // linear — width directly proportional to value
.style({ sizepow: 2 })    // area-proportional (default)
.style({ sizepow: 0.5 })  // amplification — exaggerates large values
.style({ sizepow: 10 })   // near-logarithmic — compresses wide ranges

Or implicitly through a type modifier, which overrides the style property:

Type modifier Sets sizepow Meaning
LINEAR / SIZEP1 1 Length represents the value
SIZEP1H 1.5 Between linear and area
(none — default) 2 Area represents the value
SIZEP3 / SIZEVOLUME 3 Volume represents the value
SIZEP4 4 Stronger compression
SIZEP10 / SIZELOG 10 Near-logarithmic

Effect

Higher sizepow → contrast between small and large values is compressed. Lower → exaggerated. Concretely, for two data values 100 and 10 000 (a 1:100 ratio), the rendered symbol-width ratio is:

sizepow Width ratio Visual result
0.5 1 : 10 000 Extreme exaggeration — small values disappear
1 1 : 100 Honest in width, but area differs 1 : 10 000 — reads as exaggerated
2 (default) 1 : 10 Area differs 1 : 100 — perceptually matches the data ratio
3 1 : 4.6 Compressed — good for heavy-tailed data
10 1 : 1.6 Nearly uniform — only the ordering remains visible

Use the default for most maps. Reach for 310 when a few extreme values would otherwise drown everything else (revenue, city population), and for values below 1 only when you deliberately want the largest values to dominate the picture.


dynamicScalePow — zoom rate

Controls how quickly symbols grow or shrink as the user zooms. The default is 3 (cubic root), which provides smooth transitions without extreme size changes.

Formula: DynamicScale = (1 / scaleRatio) ^ (1 / dynamicScalePow)

.options({
    objectscaling:   "dynamic",
    normalSizeScale: "8000000",
    dynamicScalePow: "3"    // default — cubic root, smooth transitions
    // dynamicScalePow: "1.8"  // more aggressive growth on zoom-in (dense datasets)
})

Higher values produce faster growth when zooming in. For dense point datasets where you want symbols to spread out noticeably on zoom, use "1.8". For sparse data where continuity matters more, keep the default "3".


Urban trees preset

A reliable starting point for street-level zoom (zoom 14–16) with species bubbles and |GLOW:

.options({
    objectscaling:   "dynamic",
    normalSizeScale: "5000"      // street scale
})
.style({
    normalsizevalue: "220",      // diameter in cm — controls relative sizing
    scale:           0.32,       // reduce glow halo size
    fillopacity:     0.8,
    showdata:        "true"
})
.type("CHART|BUBBLE|CATEGORICAL|GLOW")

Scale-dependent visibility

Hide a layer when the map is too zoomed out or too zoomed in:

.style({
    chartupper: "1:5000",   // hide when denominator > 5000 (zoomed out)
    chartlower: "1:500"     // hide when denominator < 500 (zoomed in)
})

Background boxes can also appear/disappear by scale:

.style({
    boxupper: "1:250000"    // box visible only at denominator ≤ 250 000
})

Runtime size adjustments

Change size interactively after the map loads:

// Set scale to 2x:
myMap.then(api => api.changeThemeStyle("layerName", "scale:2.0", "set"));

// Shrink by 10 %:
myMap.then(api => api.changeThemeStyle("layerName", "scale:0.9", "factor"));

// Grow grid cells by 10 %:
myMap.then(api => api.changeThemeStyle("layerName", "gridwidthpx:1.1", "factor"));