Colors & Classification

How iXMaps maps data values to colors: classification methods, the colorscheme grammar, qualitative and sequential (viridis/plasma/magma) palettes, categorical binding, COMPOSECOLOR multi-field blending, and data-driven dynamic opacity.

A CHOROPLETH or CHART layer maps a value field to a color. This guide covers the two halves of that mapping: classification (how values are grouped into classes) and color schemes (which colors those classes get).


Continuous vs. discrete classification

How the value→color mapping is done depends on whether the data is continuous numeric or discrete categorical.

Continuous numeric values — a number that can take any value in a range (population, temperature, GDP). Classification groups the range into classes and assigns a colour to each class:

Method How classes are built
EQUIDISTANT Equal-width intervals across the data range
QUANTILE Equal-count — same number of features per class
HEADTAIL Iterative mean split — best for heavy-tailed distributions
NATURAL Jenks natural breaks — minimises within-class variance
LOG Logarithmic intervals — for data spanning orders of magnitude
POW2 Equal-width intervals in square-root space — dampens high outliers less aggressively than LOG
POW3 Equal-width intervals in cube-root space — between POW2 and LOG
// Continuous: 5 quantile classes, yellow → dark blue
.type("CHOROPLETH|QUANTILE")
.style({ colorscheme: ["#ffffcc","#41b6c4","#253494"] })
NoteNATURAL cost scales with feature count

Jenks natural breaks is O(n² × classes) — fine for hundreds or a few thousand features (well under a second), but a large layer (tens of thousands of points) can take noticeably longer to classify than QUANTILE or HEADTAIL. For big datasets, try HEADTAIL first — it’s O(n) and often gives a comparably good break at the outliers.

Discrete categorical values — a label or code that belongs to a fixed set ("forest", "urban", "water"; or species names, party names, land-use codes). Use CATEGORICAL and pair it with a values array that maps each category to a colour slot:

// Discrete: three categories, each pinned to a colour
.type("CHART|BUBBLE|CATEGORICAL")
.style({
    colorscheme: ["#4fc3f7", "#ffb300", "#ef5350"],
    values:      ["low",     "medium",  "high"]   // strings — not numbers!
})
Warningvalues must be strings

iXMaps matches category values as strings. Numeric categories like 1, 2, 3 are silently ignored — always pass ["1","2","3"].

The classification methods work with both CHOROPLETH (polygon fill) and CHART (symbol colour).

In all cases — continuous or discrete — the colour scheme maps to classes in order: first colour → first class, second colour → second class, and so on. With continuous methods the classes are numeric ranges computed automatically; with CATEGORICAL the classes are the entries in the values array. The colour assignment mechanism is the same.

To set class breaks explicitly instead of computing them, use the ranges style property (n+1 values for n classes) — see Style Properties.


Color schemes

The colorscheme property accepts several formats. Underneath, it’s a small positional grammar:

colorscheme: [ classCount, colorA, colorB, param1, param2 ]

You can pass this as a real JS array, or as a string — "5,#ffffcc,#253494" (comma-delimited) or "5|#ffffcc|#253494" (pipe-delimited). A string containing RGB(...) must use | as the outer delimiter, since the color’s own internal commas would otherwise be mistaken for the grammar’s delimiters:

colorscheme: "7|RGB(74,74,255)|RGB(245,41,38)|dynamic"   // correct — pipe-delimited
colorscheme: "7,RGB(74,74,255),RGB(245,41,38),dynamic"   // wrong — commas inside RGB() corrupt the split

Any color slot also accepts a CSS color name ("cornflowerblue") or rgb(...)/rgba(...) functional notation, in addition to hex. An unrecognized color name silently falls back to white — double-check spelling if a class renders unexpectedly white.

Single color

colorscheme: ["#0066cc"]          // single hex color
colorscheme: ["blue"]             // named color

Gradient (two colors, auto-interpolated)

colorscheme: ["5", "#ffffcc", "#253494"]   // 5 classes, yellow → dark blue
colorscheme: ["#fee5d9", "#a50f15"]        // implied 2-class gradient

The first element can be a number (class count) or omitted; iXMaps infers it from the array length.

ImportantThis form takes at most 2 anchor colors — not n

["N", colorA, colorB, ...] is not a piecewise multi-stop gradient. Extra hex strings past the 2nd don’t add more anchor points; they’re read as the shape parameters described below. If you pass 5 explicit hex colors here expecting the ramp to pass through all 5, only the first two are used as the sweep’s start/end — the rest are silently ignored as malformed shape keywords. For an explicit multi-anchor ramp with no interpolation, use Multi-stop palette below instead (one color per class, no smoothing) — or the 3-color form immediately below for a smoothed start → middle → end ramp.

Gradient with an explicit middle color (3-color sweep)

A CHOROPLETH gradient isn’t limited to a straight 2-color interpolation — iXMaps always sweeps through an implicit third, middle color between the start and end, computed automatically unless you override it:

colorscheme: ["24", "#c94f35", "#4f9153"]              // red → auto middle (a pale, near-white tone) → green
colorscheme: ["24", "#c94f35", "#4f9153", "#f2d16b"]   // red → #f2d16b (yellow) → green — explicit middle
  • 3rd array element = the middle color, in one of two forms:
    • A hex string — used as the middle color directly (e.g. "#f2d16b" above). This is how to get a proper red→yellow→green-style diverging ramp from a single .style() call, without precomputing colors yourself.

    • A shape keyword controlling how the sweep is weighted across the N classes:

      Keyword Shape
      linear Straight interpolation, uniform step size — pure colorA → colorB
      dynamic / auto (default) Middle color computed automatically, weighted so low values expand
      2low / 3low Low end of the range expanded (75/25 split)
      2high / 3high High end of the range expanded (23/77 split)
      2narrow / 3narrow Compressed mid-range
      2wide / 3wide Expanded mid-range
      Note2xxx vs 3xxx — two colors or three?

      Each pair shares the same split-ratio shape — the 2/3 prefix doesn’t change the shape itself. It documents whether a middle color follows: 2low/2high/2narrow/2wide are for a plain two-color sweep with an auto-computed midpoint; 3low/3high/3narrow/3wide signal that you’re supplying an explicit middle color as the next parameter instead:

      colorscheme: ["24", "#c94f35", "#4f9153", "3low", "#f2d16b"]   // explicit middle: #f2d16b
  • 4th array element (optional) — a second shape/tuning slot: another hex to override the middle color a different way, "shift" to nudge the sequence by one step, "warm"/"cold" for a warm cream or white mid-tone shorthand, or a repeat of a shape keyword.
  • Omit the middle color entirely and iXMaps computes a sensible one automatically (generally a pale near-white/yellow tone) — reasonable for a quick look, but pass an explicit hex whenever the ramp needs to hit a specific hue (as in the red→yellow→green example above).
// Diverging-looking sequential ramp in one line — no manual color math
.type("CHOROPLETH|EQUIDISTANT")
.style({ colorscheme: ["24", "#c94f35", "#4f9153", "#f2d16b"] })

Multi-stop palette

colorscheme: ["#ffffcc","#c7e9b4","#41b6c4","#2c7fb8","#253494"]  // 5 explicit colors

No leading count, no interpolation — each color maps 1:1 to a class in order (5 colors → 5 classes), same as the general rule in Continuous vs. discrete classification above. Pair with an explicit ranges array to control exactly where those 5 class breaks fall (see Explicit class breaks). This is the right tool for a genuinely custom multi-hue palette with more than 3 named anchor colors — the gradient forms above only sweep through up to 3 (start, middle, end); beyond that, list every class color explicitly here instead.

Predefined colorschemes

Instead of generating a gradient, name a built-in palette. There are two families: qualitative palettes (a fixed list of distinct hues, best for CATEGORICAL data) and sequential colormaps (a smooth, perceptually-even ramp, best for continuous data).

Qualitative palettes

colorscheme: ["5", "tableau10"]        // 5 colors from tableau10
colorscheme: ["5", "tableau10", "3"]   // 5 colors starting at index 3
Palette Character
tableau / tableau10 / tableau20 Standard Tableau qualitative sets (10 or 20 distinct hues)
office MS-Office-style palette
mineral Muted earth-tone palette
pastel Soft, low-saturation palette
harvest Warm autumnal palette
fruit Bright, saturated palette
kmeans / kmeansp High-distinctiveness palettes generated for cluster visualization
pimp High-saturation, high-contrast palette
intense Deep, saturated palette
fluo Bright neon-toned palette

An optional 3rd array element offsets the starting index into the palette; requesting more colors than the palette has repeats it cyclically:

colorscheme: ["100", "tableau"]   // auto-palette, wraps around after 20 colors

Sequential (perceptually uniform) colormaps

For continuous data where perceptual uniformity matters (equal steps in value should look like equal steps in color), use one of the standard sequential colormaps instead:

colorscheme: ["9", "viridis"]   // dark purple → teal → yellow
colorscheme: ["9", "plasma"]    // indigo → magenta → yellow
colorscheme: ["9", "magma"]     // black → purple/red → pale yellow

Unlike the qualitative palettes above, these work for any number of classes — colors are resampled evenly across the colormap’s full range rather than sliced from a fixed list, so ["3", "viridis"] and ["256", "viridis"] both produce correctly-spanning results, not a truncated or repeating subset.

NoteThe real matplotlib-derived colormaps

viridis, plasma, and magma here are the same perceptually-uniform, colorblind-friendly colormaps used across matplotlib, D3, and most modern data-viz tooling — not an approximation. Created by Nathaniel J. Smith, Stéfan van der Walt, and Eric Firing, and released under CC0 (public domain).

Spectrum (hue-wheel) generator

A continuous rainbow-style generator, distinct from the palettes above — walks a hue wheel between two angles instead of picking from a fixed color list:

colorscheme: ["24", "spectrum", "pastel", "0", "300"]
//             classCount, "spectrum", style, hueStart, hueEnd

hueStart/hueEnd default to 270/0 if omitted. The third slot selects a style preset: default (full saturation/value), pastel, soft, hard, light, or pale.

WarningAvoid the work preset

A colorstyle: "work" preset exists in the code but is currently broken — it produces invalid colors. Use one of the presets listed above instead.

To change a spectrum colorscheme’s style preset at runtime without touching anything else, see Changing colors at runtime below.

Ready-made gradient recipes

The framework also ships a curated library of full recipe strings (color1, color2, shape, mid-color) as convenient starting points — prepend a class count to use one, e.g. "7,#ffeeee,#dd0000,dynamic,cold" for a 7-class red ramp. These are the same presets shown in the legacy visual theme-configurator’s color picker; see ui/js/tools/colorselect.js for the full list if you want to browse or copy one.

CATEGORICAL color binding

For categorical data, the values array pins specific categories to specific colors (in the same order as colorscheme):

.type("CHART|BUBBLE|CATEGORICAL")
.style({
    colorscheme: ["#4fc3f7", "#ffb300", "#ef5350"],
    values:      ["low",     "medium",  "high"]    // strings — not numbers!
})
WarningAlways use values with CATEGORICAL

Without values, iXMaps assigns colors by order of first occurrence — unpredictable and data-order dependent.

Always cast values to strings: ["1","2","3"] not [1,2,3].

TipSort categories alphabetically instead of by data order

Add ORDER to the type string (CATEGORICAL|ORDER) to assign colors to alphabetically-sorted category values, instead of the order they happen to appear in the data.

A second, independent binding mechanism exists for when the color category should come from a different field than the one driving the display/label:

.style({
    colorfield:   "region_type",                    // raw data field driving color
    colorvalues:  ["urban", "rural", "coastal"],     // fixes color-slot order, like `values` does
    colorscheme:  ["#4fc3f7", "#ffb300", "#ef5350"]
})

colorfield also accepts the special value "$index$", which colors each row by its row index rather than any field value — useful for arbitrary per-row color assignment unrelated to any real data column.

Diverging scales

Use an even number of colors and set rangecentervalue:

.style({
    colorscheme:      ["#d73027","#f46d43","#fdae61","#abd9e9","#74add1","#4575b4"],
    rangecentervalue: 0
})

The range is symmetrized around the center value regardless of class-count parity — an odd class count still works fine (one class straddles the center); an even count is a visual-design choice for a clean split either side of center, not a requirement.

Flow line gradients

For CHART|VECTOR|BEZIER|GRADIENT, use linecolor as a two-element array — not colorscheme:

.type("CHART|VECTOR|BEZIER|POINTER|GRADIENT")
.style({
    linecolor:  ["#00aabb", "#ff4400"]  // origin color → destination color
})

COMPOSECOLOR (multi-field color blending)

For layers driven by several numeric fields at once, COMPOSECOLOR blends each field’s own base color into a single composite, weighted by that field’s value — instead of picking one “dominant” field’s color like plain DOMINANT does (see Multi-field coloring modifiers):

.type("CHOROPLETH|DOMINANT|COMPOSECOLOR")
.style({
    colorscheme: ["#d73027", "#1a9850", "#4575b4"],   // one base color per field
    brightness:  0.7
})

Two blending modes, selected by an additional type flag:

Mode Type Behavior
Additive (default) COMPOSECOLOR Light-mixing — colors combine like overlapping light sources (adding channels, brighter where multiple fields are high)
Subtractive COMPOSECOLOR\|SUBTRACTIVE Pigment-mixing — colors combine like overlapping ink/paint (darker where multiple fields are high)

brightness (0–1) tunes the overall intensity of the resulting blend — lower values produce more muted composites, higher values push toward saturated combined hues.

TipWhen to reach for this

COMPOSECOLOR is well suited to genuinely multivariate choropleths — e.g. three demographic shares per polygon — where you want one glance to convey “which mix of factors dominates here” without needing a legend per field.

Data-driven colorschemes

Every format above is a static palette, fixed when you write .style({...}). For a palette computed from the layer’s own data — e.g. color assigned by matching each class’s label against a pattern, once the classes are actually known — pass a function reference instead of colors:

.style({ colorscheme: "ixmaps.colorScheme_speedmap" })

Define the function on the ixmaps namespace (same convention as userdraw — see Custom Charts). This is a real, shipping example — a CATEGORICAL speed-limit layer where the color depends on the text of each class label, not a fixed palette:

ixmaps.colorScheme_speedmap = function (theme) {
    // theme.szLabelA     — one label per class, in class order
    // theme.colorScheme  — mutate in place: one hex string per class

    for (var i = 0; i < theme.szLabelA.length; i++) {
        if (theme.szLabelA[i].match(/^(45|50|55|60|65|70|75) mph/i)) {
            theme.colorScheme[i] = "#CE517F";
        } else if (theme.szLabelA[i].match(/^40 mph/i)) {
            theme.colorScheme[i] = "#CC6166";
        } else if (theme.szLabelA[i].match(/^30/i)) {
            theme.colorScheme[i] = "#D0A148";
        }
        // ... remaining bands, then a fallback for unmatched labels
    }
};

Load the required helper script and reference the layer as usual:

ixmaps.Map("map_div", { /* ... */ })
    .require("../../ui/js/tools/colorscheme.js")   // ColorScheme.createColorScheme(), used below
    .layer(ixmaps.layer("osm", layer => layer
        .data({ type: "ext", name: "OSM_dataquery_stressmap" })
        .binding({ position: "geometry", id: "id", value: "maxspeed" })
        .type("CHART|FEATURES|CATEGORICAL|LINES")
        .style({ colorscheme: "ixmaps.colorScheme_speedmap", linewidth: "3" })
        .meta({ title: "Speed limits" })
    ));

iXMaps calls the function once the theme’s classes are resolved (theme.szLabelA populated), then reads theme.colorScheme[i] to paint each class — so this is the way to base color on values the layer only knows after loading (its actual set of categories, quantile breaks, etc.). To start from a named palette and only override specific entries, ColorScheme.createColorScheme(...) (from the same colorscheme.js tool) returns a color array you can index into before selectively overwriting entries — see ixmaps.colorScheme_surfacemap alongside colorScheme_speedmap in the source above for that combined pattern.

NoteReference by name, not by array, not inline

colorscheme: "ixmaps.myFunc" works as a bare string (["ixmaps.myFunc"] also works — both are normalized the same way). Never pass an inline function body as the string — the property parser splits on , and |, which corrupts multi-statement source.

WarningWhere the function must live

A dotted reference ("ixmaps.colorScheme_speedmap") is resolved against iXMaps’ own internal namespace. A bare, undotted function name is instead resolved against the outer embedding page’s window — so if you define your callback as a plain global function (not namespaced under ixmaps.), reference it by its bare name, not a dotted path to it.

A plain JS array built dynamically before the layer is defined works too — colorscheme: buildPalette(n) is just a normal array by the time .style() sees it — but it can’t reflect anything about data this same layer hasn’t loaded yet. Use the function form for anything that depends on the layer’s own resolved classes.

Fallback color for unmatched items

Use nodatacolor for any item with no matching class or value:

.style({ nodatacolor: "#eeeeee" })

Changing colors at runtime

map.Api.changeThemeStyle(...) recognizes a few additional properties not available at initial .style() definition time — useful for building interactive controls (dropdowns, sliders) that change an existing theme’s coloring live. See the API Reference for the general changeThemeStyle mechanism; these are the color-specific properties it accepts:

map.Api.changeThemeStyle("colorscheme:spectrum,pastel;classes:10");
Property Effect
colorscheme Replace the gradient/palette (class count carries over from the current theme)
classes Change the number of classes, keeping the same colors/generator
colorstyle Change the style preset of a spectrum colorscheme without touching anything else
colordef Set the fully-resolved color array directly, bypassing all gradient/palette generation
colorschemegeneration Live-update just the gradient’s mid-color (warm/cold/an explicit color)

Dynamic opacity

Independent of colorscheme, a layer’s fill/stroke opacity can be driven dynamically by data — letting a map show “how much” alongside “which color class” in the same fill. Add a DOPACITY* type key (see Dynamic opacity modifiers for the full list) and tune it with the dopacitypow/dopacityscale style properties (see Dynamic opacity properties):

.type("CHOROPLETH|QUANTILE|DOPACITYMAX")
.style({ dopacitypow: 1.5 })

Value binding — alphafield

By default, DOPACITY* derives opacity from the same value driving the color class. To drive opacity from a different field entirely, set alphafield:

.style({
    alphafield:  "population_density",
    dopacitypow: 1.5
})

When alphafield is set, opacity is computed purely from that field’s own min/max range across the layer — completely decoupled from whatever field is driving the color classification.

Special case — density-normalized opacity

NotePolygon area: bound field, or automatic geodesic fallback

Both mechanisms below divide by a polygon’s area, read from the FEATURE layer providing the geometry. If that layer has an explicit area field bound via .binding({ size: "area_field_name" }), that value is used as-is — it must be in square meters to match the internal km² conversion:

myMap.layer("regions")
    .data({ url: GEO_URL, type: "topojson" })
    .binding({ geo: "geometry", id: "region_id", size: "area_m2" })
    .type("FEATURE")
    .style({ colorscheme: ["none"] })
    .define();

If no size binding is present, iXMaps automatically computes each polygon’s real geodesic surface area from its own coordinates (correct for any size or location, holes correctly subtracted, antimeridian-safe) — so DENSITY and alphafield100:"$density$" work out of the box without any extra data field, at the cost of a small margin of error versus a precisely-surveyed area value (typically well under 2%, depending on how simplified the source geometry is). Bind an explicit size field only when you have an authoritative area figure you want to use instead.

Two distinct “density” mechanisms exist, easy to confuse:

Classification density — add DENSITY to the type string to convert the classification value itself into a per-area density (value ÷ polygon area) before it’s classified into color classes. This changes what the map’s colors mean (raw totals vs. density), not just the opacity.

.type("CHOROPLETH|QUANTILE|DENSITY")

Opacity density — a separate, narrower mechanism: set alphafield100 to the literal string "$density$" alongside alphafield on a CHOROPLETH (not CHART) layer. This normalizes the opacity-driving field by polygon area before computing DOPACITY* opacity — so a small dense polygon and a large sparse polygon with the same raw alphafield total get proportionally different opacity, reflecting true density rather than raw magnitude:

.type("CHOROPLETH|QUANTILE|DOPACITYMAX")
.style({
    alphafield:    "incident_count",
    alphafield100: "$density$"
})
NoteUse this when raw counts would mislead

This is the right tool when comparing polygons of very different sizes — e.g. incident counts across postal-code zones of wildly different areas — where opacity should reflect concentration, not just absolute magnitude.


Next steps