create-ixmap
Create ixMap Skill
Creates complete HTML files with interactive ixMaps visualizations for geographic data.
⚠️ CRITICAL RULES (Never Skip)
- ALWAYS assign
ixmaps.Map()toconst— discarded instance = silent failureconst myMap = ixmaps.Map("map", { ... }); // ✅ ixmaps.Map("map", { ... }); // ❌ instance lost - ALWAYS include
.binding()withgeoandvalue - ALWAYS include
showdata: "true"in.style() - ALWAYS include
.meta()with tooltip (default:{ tooltip: "{{theme.item.chart}}{{theme.item.data}}" })- Also include
namewhenever you plan to usechangeThemeStyleat runtime (see rule 21)
- Also include
- NEVER use
.tooltip()— doesn't exist - NEVER combine
CHARTandCHOROPLETHin one type string — mutually exclusive - NEVER use
|EXACTclassification — deprecated; useCATEGORICAL - NEVER use
mapas variable name — conflicts with internals; usemyMap8a. NEVER use reserved HTML element IDs — ixMaps ownsloading-div,tooltip,contextmenu. Using them causes visible artifacts (a white box stuck on the map). Useapp-loadingor any other non-conflicting name for your own overlays. - NEVER use
opacityin.style()— usefillopacity - NEVER use
fillcolor— usecolorscheme: ["#hex"] - NEVER add
.legend("string")unless user explicitly requests it — destroys the default color legend - ALWAYS use CDN
https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/ixmaps.js- data.js (
https://cdn.jsdelivr.net/gh/gjrichter/data.js@master/data.js) is already loaded by ixmaps —Data.*functions are available insidequery:andprocess:callbacks without any extra<script>tag - Only include the data.js CDN explicitly when you need
Data.*functions outside ixmaps theme realization (e.g. pre-processing data in your own<script>block before defining layers)
- data.js (
- NEVER use info from
ixmaps.caorixmaps.com— onlygithub.com/gjrichter/ixmaps-flat - ONE
.data()per layer — never chain two.data()calls on the same layer - SAME LAYER NAME for all layers sharing geometry — #1 cause of silent failures:
- ✅
myMap.layer("regions").type("FEATURE")→myMap.layer("regions").type("CHOROPLETH") - ❌
myMap.layer("regions").type("FEATURE")→myMap.layer("flows").type("CHOROPLETH")— silently broken
- ✅
- NO
FEATUREon overlay layers — base layer getsFEATURE; choropleth/chart overlays do not:- ✅
myMap.layer("x").type("FEATURE")→myMap.layer("x").type("CHOROPLETH|CATEGORICAL") - ❌
myMap.layer("x").type("FEATURE")→myMap.layer("x").type("FEATURE|CHOROPLETH|CATEGORICAL")
- ✅
objectscaling: "dynamic"requiresnormalSizeScale— set to map scale denominator: zoom 4→30M · 5→15M · 6→8M · 8→2M · 10→500k · 12→100klookupgoes in.binding(), not in.data()values:for CATEGORICAL must be strings — ixMaps bug: numeric values silently ignored- To make a fill invisible use
colorscheme: ["none"]— NOTfillopacity: 0(causes errors) - FEATURE layer:
colorschemecontrols line color — forFEATUREtype layers,colorschemesets the line/stroke color, NOT the fill.linecoloris overridden bycolorscheme. Always drive line color viacolorscheme.colorscheme: "none"silences lines entirely (renders invisible). changeThemeStylerequiresnamein.meta()— it finds themes byname, NOT by the string inmyMap.layer("name"). Withoutname, calls silently have no effect:.meta({ name: "punti", tooltip: "..." }) // ✅ — changeThemeStyle("punti", ...) will work .meta({ tooltip: "..." }) // ❌ — theme is invisible to changeThemeStylehideTheme/showThemealso resolve themes bynamein.meta()— same rule aschangeThemeStyle. Oncenameis set, useixmaps.hideTheme(name)/ixmaps.showTheme(name)for layer visibility. CSS injection ([id*=":name:"] { display: none !important }) remains a reliable fallback ifhideThemebehaves unexpectedly for a given layer type.
Choosing Visualization Type
Is your data...
├─ Points (lat/lon)?
│ ├─ Just locations? → CHART|DOT
│ ├─ Colored by category (legend-selectable)? → CHART|BUBBLE|CATEGORICAL ⚠️ NOT DOT|CATEGORICAL
│ ├─ Sized by value? → CHART|BUBBLE|SIZE|VALUES
│ ├─ Density heatmap (circles)? → CHART|BUBBLE|SIZE|AGGREGATE + gridwidth:"5px"
│ ├─ Density heatmap (squares)? → CHART|SYMBOL|GRIDSIZE|AGGREGATE|RECT|SUM|DOPACITY|VALUES + symbols:["square"] + gridwidth:"80px"
│ ├─ Sparklines per grid cell? → CHART|SYMBOL|PLOT|LINES (see Sparklines below)
│ ├─ Flows origin→destination? → CHART|VECTOR|BEZIER|POINTER
│ ├─ Multi-value per point? → CHART|SYMBOL|SEQUENCE (|STAR for 5+ categories)
│ └─ Stacked/grouped bars per location? → CHART|BAR|STACKED (add |SIZE|GRID|BOX|VALUES for full display)
│ gridx:N in .style() = values per bar group (gridx:2 → 2 segments per bar; gridx:3 → 3 separate bars)
│
└─ Polygons (GeoJSON/TopoJSON)?
├─ Boundaries only? → FEATURE
├─ Colored by data (geometry+data)? → FEATURE|CHOROPLETH (|QUANTILE | |EQUIDISTANT | |CATEGORICAL)
└─ Data joined to pre-loaded geometry?→ CHOROPLETH only — NEVER FEATURE|CHOROPLETH
Key type modifiers:
|GLOW— glow effect on any CHART type|DOPACITYMAX— dynamic opacity (high values prominent); addalpha: "field"to.binding()|DOPACITYMINMAX— dynamic opacity (extremes prominent)|CATEGORICAL— discrete category coloring;values:array in style maps tocolorschemein order|SILENT— excludes layer from legend, statistics and suppresses tooltips on its items|NOLEGEND— excludes layer from legend only (tooltips still work)|NOOUTLIER— removes extreme outliers from classification calculations|ZEROISNOTVALUE— suppresses rendering where value ≤ 0 (useful for sparse/incomplete time series)|NOSCALE— disables dynamic zoom scaling; flows/symbols stay constant size regardless of zoom|GRADIENT— gradient color along flow lines (origin color → destination color); use withCHART|VECTOR|BEZIER— gradient must be defined vialinecolor: ["#from","#to"]array, NOTcolorscheme|CLIPTOGEOBOUNDS— clips chart rendering to the containing polygon boundary|DOMINANT|PERCENTOFMEAN— colors by which of multiple piped fields is above-mean dominant; useful for showing "winner" category per region|DTEXT— makesVALUES-generated text labels onCHOROPLETHthemes properly sized (always pair with|VALUESon choropleth layers that show value labels)|SMOOTH— smoothing interpolation on sparkline curves|SORT/|SORT|DOWN— sort sparkline categories ascending / descending|TEXTLEGEND— renders category labels directly on chart symbols instead of in the legend box|TEXTONLY— text labels only, no chart symbol (combine withCHART|LABEL|VALUES|FIXSIZE|NOLEGEND)
Aggregation modifiers (replace the value of each cell with the aggregate):
| Modifier | Computes |
|---|---|
SUM |
Sum of all values in cell |
COUNT |
Count of rows in cell |
MEAN |
Arithmetic mean |
MIN |
Minimum value |
MAX |
Maximum value |
Classification methods (used with CHOROPLETH and CHART):
| Method | Description |
|---|---|
EQUIDISTANT |
Equal-width intervals across the data range |
QUANTILE |
Equal-count intervals — each class has the same number of features |
HEADTAIL |
Head/tail breaks — iteratively splits at the mean; best for heavy-tailed distributions |
NATURAL |
Jenks natural breaks — minimises within-class variance |
LOG |
Logarithmic intervals — useful when values span several orders of magnitude |
VECTOR sub-modifiers:
|DASH— animated flowing dashes along flow direction (combine freely with BEZIER|POINTER|FADEIN)|GRADIENT— gradient color from origin to destination along each flow line
Deprecated modifiers — do NOT use:
→ useSIZEP1SIZE+sizepow: 1in.style()instead→ useEXACTCATEGORICALinstead
For full type-string reference and all modifiers → API_REFERENCE.md § Visualization Types
Workflow
- Parse the user's request: data source, visualization goal, styling preferences
- Ask if key info is missing (data format? geographic scope?)
- Choose template:
template-points.html— CSV/JSON with lat/lontemplate-geojson.html— GeoJSON/TopoJSONtemplate-multi-layer.html— multiple layers with jointemplate.html— general purpose
- Write the HTML file
- Validate before writing:
-
const myMap = ixmaps.Map(...)— instance stored -
.binding()hasgeo+value -
.style()hasshowdata: "true" -
.meta()present with tooltip - If
objectscaling:"dynamic"→normalSizeScaleset - Start with
scale: 1— let user request size adjustments
-
- Confirm file created; explain what it shows; offer to enhance
Defaults
| Setting | Default |
|---|---|
| filename | ixmap.html |
| mapType | "VT_TONER_LITE" ← always use unless user asks otherwise |
| center | { lat: 42.5, lng: 12.5 } (Italy) |
| zoom | 6 |
| colorscheme | ["#0066cc"] |
| basemapopacity | 0.6 |
| flushChartDraw | 1000000 |
| flushPaintShape | (not set) — set to 1000000 when rendering large polygon datasets (municipalities, communes) to avoid rendering hangs |
| zoomAnimation | true — smooth zoom transitions; set false to disable |
| tools | true |
Valid basemaps (case-sensitive): "VT_TONER_LITE" · "white" · "CartoDB - Dark matter" · "CartoDB - Positron" · "Stamen Terrain" · "OpenStreetMap - Osmarenderer"
❌ NOT: "OpenStreetMap" · "OSM" · "CartoDB Positron" → See MAP_TYPES_GUIDE.md for full list
Map Init Pattern
⚠️ Scrollbar pitfall — never use
width: 100vw; height: 100vhon the map<div>. When a scrollbar appears,vw/vhexceed the viewport and trigger a feedback loop. Always use:html, body { width: 100%; height: 100%; overflow: hidden; } #map { width: 100%; height: 100%; }
const myMap = ixmaps.Map("map", {
mapType: "VT_TONER_LITE",
mode: "info",
legend: "closed", // or "open"
tools: true
})
.view({ center: { lat: 42.5, lng: 12.5 }, zoom: 6 })
.options({
objectscaling: "dynamic",
normalSizeScale: "8000000", // match to zoom (zoom6≈8M, zoom12≈100k)
basemapopacity: 0.6,
flushChartDraw: 1000000
});
Lambert projection (Eurostat style)
var myMap = ixmaps.Map("map", {
mapType: "white", // blank tile layer
mapProjection: "lambert", // Lambert Azimuthal Equal-Area — EPSG:3035
mode: "pan",
legend: "closed",
tools: false
})
.view([53.4, 16.9], 3.7) // ⚠️ ARRAY syntax [lat, lng], zoom — object {center,zoom} does NOT work with projections
.options({ basemapopacity: 0, flushChartDraw: 1000000 });
- Set sea/background color via CSS on the
#mapdiv:background: #c6daea basemapopacity: 0hides tile layer entirely- Add a graticule layer before countries (see Graticule below)
Graticule (world grid lines)
(function() {
var step = 10, features = [];
for (var lon = -180; lon <= 180; lon += step) {
var coords = [];
for (var lat = -90; lat <= 90; lat += 2) coords.push([lon, lat]);
features.push({ type:"Feature", geometry:{ type:"LineString", coordinates:coords }, properties:{} });
}
for (var lat = -80; lat <= 80; lat += step) {
var coords = [];
for (var lon = -180; lon <= 180; lon += 2) coords.push([lon, lat]);
features.push({ type:"Feature", geometry:{ type:"LineString", coordinates:coords }, properties:{} });
}
myMap.layer("graticule")
.data({ obj: { type:"FeatureCollection", features:features }, type: "geojson" })
.binding({ geo: "geometry" })
.type("FEATURE|SILENT")
.style({ colorscheme: "#7aaabb", linewidth: 0.6, fillopacity: 0 })
.define();
})();
Intermediate points every 2° ensure smooth curves in Lambert projection. Define graticule before the countries layer so it renders underneath.
Layer chain (order matters):
myMap.layer("name")
.data({ url: "…", type: "csv" }) // OR obj: myArray
.binding({ geo: "lat|lon", value: "fieldname", title: "label" })
.filter('WHERE field == "value"') // optional; use AND/OR not && /||
.type("CHART|BUBBLE|SIZE|VALUES")
.style({ colorscheme: ["#0066cc"], fillopacity: 0.7, showdata: "true" })
.meta({ tooltip: "{{label}}: {{fieldname}}" })
.title("Legend label")
.define();
Full
.options()/.style()property reference → API_REFERENCE.md § Map Constructor and § Style Properties
Tooltip Mustache Reference
Tooltips in .meta({ tooltip: "..." }) use {{…}} placeholders. Two prefixes control formatting:
| Syntax | Behaviour |
|---|---|
{{fieldname}} |
ixmaps-formatted value — may apply number formatting, units, rounding |
{{raw.fieldname}} |
Raw unformatted value — bypasses all ixmaps formatting; use this when you want pre-formatted strings (e.g. "1.234.567" from .toLocaleString()) or exact string values |
{{theme.item.chart}} |
Renders the built-in chart SVG/HTML for this item |
{{theme.item.data}} |
Renders the built-in data table for this item |
raw. is the escape hatch — whenever ixmaps mangles a value (reformats numbers, truncates strings, adds units), use {{raw.field}} to get the original data value unchanged.
For fields not in the primary value binding, list them in datafields in .style() to make them available:
.style({ datafields: ["field1", "field2"], showdata: "true" })
.meta({ tooltip: "{{raw.field1}} — {{field2}}" })
Geometry Sources
geo: "geometry" for GeoJSON point data — works correctly with all CHART types.
ixmaps extracts full-precision coordinates directly from Point.coordinates[lon,lat].
The .type() call (CHART|DOT, CHART|BUBBLE, etc.) controls the renderer — NOT the geo binding.
Use geo: "geometry" when source GeoJSON has Point geometry (preferred over property lat/lon fields which may be truncated).
Only use geo: "lat|lon" when the data has separate lat/lon columns (CSV, non-geometry JSON).
World countries (GISCO — preferred over world-atlas)
.data({ url: "https://gisco-services.ec.europa.eu/distribution/v2/countries/topojson/CNTR_RG_60M_2020_4326.json", type: "topojson" })
.binding({ geo: "geometry", id: "CNTR_ID", title: "NAME_ENGL" })
// ⚠️ Join field is CNTR_ID (ISO-2) — NOT CNTR_CODE
Scales: 60M (default/world) · 20M · 10M · 3M · 1M (country zoom)
Germany municipalities (LAU 2021)
.data({ url: "https://cdn.jsdelivr.net/gh/gjrichter/geo@028b3fe/lau/germany_lau_2021_4326.topojson", type: "topojson" })
.binding({ geo: "geometry", id: "LAU_ID", title: "LAU_NAME" })
// LAU_ID = 8-digit AGS · LAU_NAME = name · POP_DENS_2021 = density (useful for alpha/DOPACITYMAX)
NUTS1 Germany
.data({ url: "https://gisco-services.ec.europa.eu/distribution/v2/nuts/topojson/NUTS_RG_60M_2021_4326_LEVL_1.json", type: "topojson" })
.filter('WHERE CNTR_CODE == "DE"')
.binding({ geo: "geometry", id: "NUTS_ID", title: "NUTS_NAME" })
// NUTS_ID examples: "DE1", "DEA" (CNTR_CODE works for NUTS, unlike country data which uses CNTR_ID)
Italy geometry sources (gjrichter/geo)
Municipalities (comuni) — ISTAT, ~8 000 polygons, 500m simplified:
.data({ url: "https://raw.githubusercontent.com/gjrichter/geo/main/italy/boundaries/italy_istat_municipalities_4326_500m.topojson", type: "topojson" })
.binding({ geo: "geometry", id: "com_istat_code", title: "name" })
// Join field: com_istat_code (numeric) — matches cod_istat in ISTAT/ondata CSVs
// Useful properties: com_istat_code, name, prov_istat_code, reg_istat_code (cod_reg for region filter)
⚠️ Use flushPaintShape: 1000000 in .options() when rendering all 8 000 polygons to avoid hangs.
⚠️ Local
file://URLs are blocked by browser CORS — always use CDN or inlineobj:Full geometry sources list → API_REFERENCE.md § Data Configuration
Multi-Layer Join Pattern
When joining external data to geometry (e.g. TopoJSON + CSV statistics):
// Step 1 — FEATURE base (geometry + id field for join)
myMap.layer("regions")
.data({ url: "regions.topojson", type: "topojson" })
.binding({ geo: "geometry", id: "reg_code", title: "reg_name" })
.type("FEATURE")
.style({ colorscheme: ["#ccc"], fillopacity: 0.1, linecolor: "#666", linewidth: 0.5, showdata: "true" })
.define();
// Step 2 — CHOROPLETH overlay (SAME layer name, NO FEATURE, lookup joins to id)
myMap.layer("regions")
.data({ url: "data.csv", type: "csv" })
.binding({ lookup: "csv_code_col", value: "metric" })
.type("CHOROPLETH|QUANTILE")
.style({ colorscheme: ["#eee", "#00468b"], fillopacity: 0.75, showdata: "true" })
.meta({ tooltip: "{{reg_name}}: {{metric}}" })
.define();
Critical: id values in geometry must match lookup values in CSV exactly (case-sensitive).
Always inspect both sources to confirm field names before writing the join.
Sparklines (CHART|SYMBOL|PLOT|LINES)
Two distinct patterns depending on data shape:
Pattern A — single column, year as category (raw events)
.binding({ geo: "lat|lon", value: "year" }) // year field = categorical x-axis
.type("CHART|SYMBOL|PLOT|LINES|AREA|FADE|LASTARROW|NOCLIP|GRIDSIZE|CATEGORICAL|AGGREGATE|RECT|SUM|FIXSIZE")
.style({
gridwidth: "100px", normalsizevalue: "30", markersize: 2,
colorscheme: ["#00e5ff"], fillopacity: 0.5,
values: ["2020","2021","2022","2023"], // ordered x-axis categories (also controls sort)
showdata: "true"
})
// CATEGORICAL+AGGREGATE+RECT+SUM = aggregation semantics (NOT style)
// AREA|FADE|LASTARROW|FIXSIZE = visual style only
// FIXSIZE: all sparks same size; normalsizevalue controls chart scale (larger = smaller sparks)
// markersize: controls LASTARROW arrow head size (default ~8; use 1–3 for smaller arrows)
// ⚠️ normalsizevalue does NOT control arrow size — use markersize for that
// LASTARROW = arrow marker on last point | LASTPOP = dot/pop marker on last point (use one or the other)
// MAX/MIN/MEAN/COUNT/SUM = aggregation modifiers — compute cell aggregate value; NOT sparkline visual markers
BOX|GRID and XAXIS — only add on explicit user request:
- Default (no BOX|GRID): sparkline appears as a lightweight curve/arrow on the map; xaxis/chart still visible in tooltip via
{{theme.item.chart}} BOX|GRID|XAXIS+label:[]+xaxis:[]in style: renders grid boxes + x-axis labels ON the map — heavier, less performant; use only when user wants to see the grid/axes directly on the mapBOXalone (without GRID): adds a background box; can be combined withTITLEorBOTTOMTITLEfor chart titles; scale-dependent viaboxupper/boxlowerandtitleupper/titlelowerstyle params
// Only when user explicitly wants grid + axis labels on map:
.type("...NOCLIP|BOX|GRID|GRIDSIZE|XAXIS|CATEGORICAL|AGGREGATE|RECT|SUM|FIXSIZE")
.style({
values: ["2020","2021","2022","2023"],
label: ["2020","2021","2022","2023"],
xaxis: ["2020","2021","2022","2023"],
})
Pattern B — multiple pre-aggregated columns
.binding({ geo: "lat|lon", value: "val2020|val2021|val2022|val2023" }) // chain columns
.type("CHART|SYMBOL|PLOT|LINES|AREA|FADE|LASTARROW|NOCLIP|GRIDSIZE|FIXSIZE")
// No CATEGORICAL or SUM — data already aggregated
Full sparkline reference, FIXSIZE/normalsizevalue details, point-anchored variant → API_REFERENCE.md § CHART|SYMBOL|PLOT|LINES
Animated / Timeseries Maps
Method A — myMap.layer(theme, "direct") (preferred)
// ixmaps.layer() (global) builds theme WITHOUT adding to map
// myMap.layer(theme, "direct") = smart upsert: add on first call, replace on subsequent
function showYear(year) {
const theme = ixmaps.layer("countries")
.data({ obj: yearData[year], type: "json" })
.binding({ geo: "lat|lon", value: "metric" })
.type("CHART|BUBBLE|SIZE|VALUES")
.style({ colorscheme: ["#0066cc"], fillopacity: 0.7, showdata: "true" })
.meta({ name: "myTheme", tooltip: "{{label}}: {{metric}}" })
.define(); // returns theme object, does NOT add to map
myMap.layer(theme, "direct"); // smart upsert — no tracking needed
}
showYear("2023");
Method B — explicit addTheme / replaceTheme
let activeTheme = null;
let mapInstance = null;
myMap.then(map => { mapInstance = map; showYear("2023"); });
function showYear(year) {
if (!mapInstance) return;
const theme = ixmaps.layer("countries")
.data({ obj: yearData[year], type: "json" })
.binding({ geo: "lat|lon", value: "metric" })
.type("CHART|BUBBLE|SIZE|VALUES")
.style({ colorscheme: ["#0066cc"], fillopacity: 0.7, showdata: "true" })
.meta({ name: "myTheme", tooltip: "{{label}}: {{metric}}" })
.define();
if (activeTheme) mapInstance.replaceTheme("myTheme", theme, "direct");
else mapInstance.addTheme("myTheme", theme, "direct");
activeTheme = theme;
}
Key: replaceTheme avoids flicker vs remove+add. Theme name in .meta() is the upsert key.
Time slider (
timefieldin.binding()),setThemeTimeFrame()→ API_REFERENCE.md § Time Slider
Key Style Properties (quick ref)
| Property | Notes |
|---|---|
colorscheme |
Array of hex colors. ["100","tableau"] for auto-palette |
fillopacity |
0–1. NEVER use opacity |
linecolor / linewidth |
NEVER strokecolor / strokewidth; linecolor accepts a single string or an array ["#c1","#c2"] — array form required for VECTOR|GRADIENT |
scale |
Uniform size multiplier (start at 1) |
normalsizevalue |
Data value that maps to "normal" display size. Higher = SMALLER bubbles — a larger reference value means most real data values fall below it, so bubbles render smaller. E.g. "1000" → smaller bubbles than "300". |
gridwidth |
Grid cell size for aggregate layers (e.g. "5px") |
rangecentervalue |
Diverging center; requires EVEN number of colors |
ranges |
Explicit class breaks (n+1 values for n colors) |
values |
Category list for CATEGORICAL (must be strings) |
align |
Chart anchor: "left" "right" "top" "bottom" "above" "below" |
sizepow |
Power curve for size scaling — 2 = quadratic, exaggerates contrast between small and large values |
rotation |
Rotate chart symbol in degrees (e.g. 35 for a tilted arrow) |
rangescale |
Scale factor applied after range computation |
aggregationfield |
Field used as aggregation key when AGGREGATE is set |
titlefield |
Field used as chart title label (overrides binding title) |
datafields |
Array of extra fields carried through to tooltip: ["field1","field2"] — access as {{raw.field1}} |
textscale |
Scale factor for label text rendered on the chart |
boxupper / boxlower |
Scale-dependent box visibility threshold, e.g. "1:250000" — box shown only when map scale ≤ 1:250k |
valuesupper / valueslower |
Scale-dependent value label visibility threshold |
valuedecimals |
Decimal places for rendered value labels |
minvaluesize |
Minimum pixel size below which no chart symbol is drawn |
units |
String appended to rendered value labels, e.g. "%", "€", "km" |
sizefield |
Data column that drives symbol SIZE independently from the value (color) field — use with CATEGORICAL to combine category color + numeric size on one layer |
dopacitypow |
Power curve exponent for DOPACITY opacity mapping (default ≈ 1; 2 = quadratic, exaggerates contrast) |
dopacityscale |
Multiplier applied after opacity calculation — stretches the opacity range |
gridwidthpx |
Grid cell width in pixels; supports "factor" mode in changeThemeStyle for runtime zoom-scaling |
Trees (street-level) sizing baseline with |GLOW:
- Use this as a reliable starting point for urban tree inventories (diameter in cm):
objectscaling: "dynamic"normalSizeScale: "5000"(street-detail zoom reference)normalsizevalue: "220"(higher value keeps bubbles controlled)scale: 0.32(reduce apparent size added by|GLOW)
- Rule of thumb: with
|GLOW, start with a smallerscalethan non-glow bubbles.
Complete style properties, dynamic opacity, diverging scales, categorical color binding → API_REFERENCE.md § Style Properties
Runtime Controls (Filters & Layer Toggles)
Use these patterns when you need interactive UI controls (checkboxes, dropdowns) that modify the map after it's loaded.
Filtering data across all layers — changeThemeStyle
changeThemeStyle(themeName, styleString, mode) modifies a live layer property and triggers a re-render. For aggregate layers (grid counts, sparklines) it also re-aggregates — cells recount correctly with only the filtered rows.
Mode values:
| Mode | Behaviour |
|---|---|
"set" |
Replace property with the given value (default) |
"remove" |
Delete the property entirely |
"factor" |
Multiply the current numeric value by the given factor (e.g. "gridwidthpx:1.1" → 10% larger) |
"set|silent" |
Set value WITHOUT triggering a redraw (use for low-priority zoom tweaks) |
Prerequisites:
- Every layer that should respond must have
namein its.meta()(see Rule 21) - Must call via the Promise API —
myMap.then(map => ...)— NOT the fluent chain
function applyFilter(activeValues) {
// activeValues = array of selected values, e.g. ["M", "F"]
const szFilter = (activeValues.length === totalCount)
? null // all selected → remove filter
: 'WHERE fieldName in (' + activeValues.join(',') + ')';
myMap.then(function(map) {
['layerNameA', 'layerNameB', 'layerNameC'].forEach(function(id) {
if (szFilter) {
map.changeThemeStyle(id, 'filter:' + szFilter, 'set');
} else {
map.changeThemeStyle(id, 'filter', 'remove');
}
});
});
}
⚠️
ixmaps.map().changeThemeStyle()returns{szMap: null}and silently does nothing — that form cannot find the live map instance.
Region selector with zoom navigation
Use a <select> dropdown to filter all theme layers to a single geographic region and pan/zoom to it. The map needs to be declared as var myMap (not const) in outer scope so both buildMap() and changeRegion() can access it.
Key facts:
- Filter uses single-value equality:
WHERE field = value - Empty
<option value="">is the "show all" sentinel — triggers filter removal - Navigation uses
myMap.view()called outside.then()— it is safe to call on the fluent chain after init myMap.view()only pans/zooms; it does not reset layers
REGION_VIEWS lookup table:
const REGION_VIEWS = {
"": { lat: 42.5, lng: 12.5, zoom: 6 }, // full extent
"1": { lat: 44.9, lng: 7.9, zoom: 8 },
// ... one entry per region code
};
changeRegion function:
var myMap; // outer scope — shared by buildMap() and changeRegion()
function changeRegion(code) {
var THEME_NAMES = ["themeA", "themeB", "themeC"]; // all named themes that should filter
var filterStr = code ? "WHERE regionField = " + code : null;
var v = REGION_VIEWS[code] || REGION_VIEWS[""];
myMap.then(function(m) {
THEME_NAMES.forEach(function(name) {
if (filterStr) {
m.changeThemeStyle(name, "filter:" + filterStr, "set");
} else {
m.changeThemeStyle(name, "filter", "remove");
}
});
});
myMap.view({ center: { lat: v.lat, lng: v.lng }, zoom: v.zoom });
}
function buildMap() {
myMap = ixmaps.Map("map", { ... });
// ...layers...
}
Overlay selector UI — centered over the map, no background bar, map interaction passes through the wrapper:
<!-- CSS -->
#region-bar {
position: absolute;
top: 20px; left: 50%; transform: translateX(-50%);
z-index: 1001;
display: flex; align-items: center; gap: 8px;
pointer-events: none; /* wrapper is click-through */
}
#region-bar label {
color: #333; font-size: 0.78rem;
pointer-events: none;
}
#region-select {
background: rgba(20,20,20,0.72);
border: 1px solid rgba(255,255,255,0.22);
border-radius: 6px; color: #f0f0f0;
padding: 5px 10px; cursor: pointer;
pointer-events: all; /* select itself is interactive */
}
#region-select option { background: #1e1e1e; color: #f0f0f0; }
<!-- HTML (inside the 1024px container div, above the map div) -->
<div id="region-bar">
<label for="region-select">Regione:</label>
<select id="region-select" onchange="changeRegion(this.value)">
<option value="">— Tutta Italia —</option>
<option value="1">Piemonte</option>
<!-- ... -->
</select>
</div>
⚠️ Always include
<option value="">as the first option — it is the "show all" state that triggersfilter remove. Presetting a region on load viaselectedmeans removing the initial.filter()from layer definitions; conversely, if a region is pre-filtered in.filter(), setselectedon the matching option so the UI and the data stay in sync.
Toggling layer visibility — hideTheme / showTheme
hideTheme and showTheme resolve themes by name in .meta() — just like changeThemeStyle. Once name is set on every layer, the standard calls work:
ixmaps.hideTheme("grid"); // hides layer named "grid"
ixmaps.showTheme("grid"); // shows it again
// Usage: <input type="checkbox" onchange="this.checked ? ixmaps.showTheme('grid') : ixmaps.hideTheme('grid')">
Initially hidden layer — add visible: false to .style() — do NOT call hideTheme from myMap.then():
myMap.layer("danno")
.binding({ ... })
.type("CHART|BUBBLE|CATEGORICAL|GLOW")
.style({
colorscheme: [...],
values: [...],
visible: false // ✅ layer starts hidden; toggle via showTheme/hideTheme at runtime
})
.define();
// ❌ WRONG: myMap.then(function() { ixmaps.hideTheme('danno'); }); — unreliable timing
CSS injection fallback — if hideTheme behaves unexpectedly for a layer type, inject/remove a style rule instead:
function toggleLayer(id, show) {
const styleId = 'hide-' + id;
if (!show) {
if (!document.getElementById(styleId)) {
const s = document.createElement('style');
s.id = styleId;
s.textContent = '[id*=":' + id + ':"] { display: none !important; }';
document.head.appendChild(s);
}
} else {
document.getElementById(styleId)?.remove();
}
}
The category filter and layer-visibility toggle work independently and can be freely combined.
Isolating categorical classes — markThemeClass / unmarkThemeClass
Use these to isolate one or more categorical classes in a CATEGORICAL layer. Marked classes stay visible; all others are hidden. When zero classes are marked, every class is shown again — no reset call needed.
// Mark (isolate) class at index n — index = position in the `values:` array (0-based)
ixmaps.markThemeClass("themeName", n);
// Remove the isolation for class n
ixmaps.unmarkThemeClass("themeName", n);
Clickable legend pattern — toggle isolation on click, track state in a Set:
const markedClasses = new Set();
function toggleClass(classIdx) {
if (markedClasses.has(classIdx)) {
markedClasses.delete(classIdx);
ixmaps.unmarkThemeClass("myLayer", classIdx);
} else {
markedClasses.add(classIdx);
ixmaps.markThemeClass("myLayer", classIdx);
}
// update legend UI: dim items not in markedClasses (only when set is non-empty)
document.querySelectorAll(".leg-item").forEach(el => {
const c = parseInt(el.dataset.class, 10);
el.classList.toggle("off", markedClasses.size > 0 && !markedClasses.has(c));
});
}
// HTML: <div class="leg-item" data-class="0" onclick="toggleClass(0)">…</div>
Multiple classes can be marked simultaneously — all marked classes show together. The
themeNamemust matchnamein.meta()(same rule aschangeThemeStyle).
Reacting to zoom / pan — .on() events
Use .on(events, handler) to subscribe to view events. Multiple space-separated events are accepted in one call.
View events
| Event | Fires when |
|---|---|
zoomend |
Zoom level changed |
moveend |
Map panned without zoom change |
viewchange (alias zoompan) |
Any zoom or pan |
Handler receives { nZoom, zoomChanged, panChanged, szMap }.
Typical zoom-adaptive pattern — debounce to avoid firing on every intermediate step:
var _zoomTimer = null;
myMap.on("zoomend moveend", function() {
clearTimeout(_zoomTimer);
_zoomTimer = setTimeout(function() {
var z = ixmaps.getZoom(); // global, no .then() needed
myMap.then(function(m) {
m.setBasemapOpacity(Math.max(0, Math.min(0.8, (z - 9) / 3)), "absolute");
m.changeThemeStyle("layerName", "minvaluesize:" + (z > 10 ? 1 : 15), "set");
});
}, 400);
});
Item (feature) events
| Event | Fires when | Handler receives |
|---|---|---|
mouseover / itemover |
Pointer enters a feature | { szId, id, theme, szMap } |
mouseout / itemout |
Pointer leaves a feature | same |
click / itemclick |
Feature clicked | same |
szId = full compound id "themeId::itemKey" · id = item key only · theme = layer id
Lifecycle events
| Event | Fires when |
|---|---|
ready / mapready |
SVG engine fully loaded |
layerdraw / drawtheme |
A layer finishes drawing |
layeradd / newtheme |
A layer is created |
layerremove / removetheme |
A layer is removed |
myMap
.on("ready", function() { hideSpinner(); })
.on("layerdraw", function(e) { console.log("drawn:", e.id); })
.on("click", function(e) { showDetail(e.id); })
.on("mouseover", function(e) { highlight(e.id); })
.on("mouseout", function() { clearHighlight(); });
Inside handlers — call ixmaps.getZoom() / ixmaps.getCenter() directly (no Promise); use myMap.then(m => ...) only when you need to call m.changeThemeStyle() or m.setBasemapOpacity().
getBounds() note — returns a flat 4-element array [swLat, swLng, neLat, neLng], NOT a Leaflet LatLngBounds object. Always guard: if (!bounds || bounds.length !== 4) return;
Legacy hook — ixmaps.htmlgui_onZoomAndPan — still works; prefer .on() for new code:
ixmaps.htmlgui_onZoomAndPan = function() {
myMap.then(function(m) { updateLegend(m.getBounds()); });
};
When another handler already owns htmlgui_onZoomAndPan, wrap it to call _prev first instead of overwriting.
Live legend pattern — update sidebar counts from inline data on every pan/zoom:
myMap.on("viewchange", function() {
myMap.then(function(m) { updateLegend(m.getBounds()); });
});
// Also fire once on load:
myMap.then(function(m) { updateLegend(m.getBounds()); });
function updateLegend(bounds) {
if (!bounds || bounds.length !== 4) return;
const [swLat, swLng, neLat, neLng] = bounds;
const counts = {};
for (const t of DATA) {
if (t.lat < swLat || t.lat > neLat || t.lon < swLng || t.lon > neLng) continue;
counts[t.category] = (counts[t.category] || 0) + 1;
}
// update DOM legend elements with new counts
}
Persisting the map view in the browser URL
Storing lat/lng/zoom in URL params lets users bookmark or share the exact view. Use ixmaps.getCenter() and ixmaps.getZoom() (global, no Promise needed) to read state, and history.replaceState to update silently.
Important: if another handler (e.g. a data provider) already owns htmlgui_onZoomAndPan, use a wrapper that calls _prev first — never overwrite blindly.
/* ── 1. Read initial view from URL (before map init) ── */
var _urlParams = new URLSearchParams(window.location.search);
var _initLat = parseFloat(_urlParams.get("lat")) || 46.8; // default fallback
var _initLng = parseFloat(_urlParams.get("lng")) || 2.3;
var _initZoom = parseFloat(_urlParams.get("zoom")) || 6;
const myMap = ixmaps.Map("map", { ... })
.view({ center: { lat: _initLat, lng: _initLng }, zoom: _initZoom })
...
/* ── 2. Write current view back to URL (debounced) ── */
var _urlUpdateTimer = null;
function updateUrlFromView() {
try {
var c = ixmaps.getCenter();
var z = ixmaps.getZoom();
if (!c || z == null) { return; }
var params = new URLSearchParams(window.location.search);
params.set("lat", c.lat.toFixed(6));
params.set("lng", c.lng.toFixed(6));
params.set("zoom", z.toFixed(4));
history.replaceState(null, "", "?" + params.toString());
} catch(e) {}
}
/* ── 3. Wrap the existing htmlgui_onZoomAndPan (don't replace it) ── */
function hookUrlUpdate() {
var _prev = ixmaps.htmlgui_onZoomAndPan; // save whatever is already there
ixmaps.htmlgui_onZoomAndPan = function(nZoom) {
try { if (_prev) { _prev.call(this, nZoom); } } catch(e) {}
clearTimeout(_urlUpdateTimer);
_urlUpdateTimer = setTimeout(updateUrlFromView, 400);
};
}
/* ── 4. Install after map is ready; setTimeout fallback for edge cases ── */
myMap.then(function() { hookUrlUpdate(); updateUrlFromView(); });
setTimeout(function() { hookUrlUpdate(); updateUrlFromView(); }, 1000);
ixmaps.getCenter()/ixmaps.getZoom()are global — call them directly, nomyMap.then()needed. Shareable URL format:map.html?lat=48.856900&lng=2.347800&zoom=14.0000
Special Patterns (quick ref)
Categorical color binding (pin specific colors to values):
.type("CHART|BUBBLE|CATEGORICAL")
.style({ colorscheme: ["#4fc3f7","#ffb300","#ef5350"], values: ["C","F","R"], showdata: "true" })
⚠️ Always use
valueswith CATEGORICAL — without it, ixMaps assigns colors by order of first occurrence in the dataset, not by category name. This means color assignments change depending on data order and are unpredictable.valuesis the only reliable way to pin a specific color to a specific category.
CATEGORICAL + bubble size from a numeric field (color by category AND size by value — single layer):
.binding({ geo: "lat|lon", value: "categoryField", title: "label", size: "numericField" })
.type("CHART|BUBBLE|CATEGORICAL|GLOW")
.style({ colorscheme: ["#4fc3f7","#ffb300","#ef5350"], values: ["C","F","R"], normalsizevalue: "80", showdata: "true" })
Add
size: "numericField"to.binding()to drive bubble radius from a numeric column independently from the categoryvaluefield. This avoids needing a separateSIZE|VALUESlayer when you want both category color and numeric sizing.
Urban trees preset (species color + diameter size + GLOW):
.binding({ geo: "lat|lon", value: "SPECIE", size: "DIAMETRO", title: "LUOGO" })
.type("CHART|BUBBLE|CATEGORICAL|GLOW")
.style({
colorscheme: [...],
values: [...], // species list, fixed order
normalsizevalue: "220",
scale: 0.32,
fillopacity: 0.8,
showdata: "true"
})
// map options: objectscaling:"dynamic", normalSizeScale:"5000"
Dynamic opacity from a field:
.type("CHART|BUBBLE|SIZE|DOPACITYMAX")
.binding({ geo: "lat|lon", value: "count", alpha: "density" })
Glow effect: add |GLOW to any CHART type
Flows with animated dashes: CHART|VECTOR|BEZIER|POINTER|DASH
CHART|USER — custom draw functions (pinnacleChart, arrowChart):
Requires three extra CDN scripts (load order relative to ixmaps.js does not matter):
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/usercharts/d3/chart.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/usercharts/d3/arrow_chart.js"></script>
The userdraw style property names the draw function ("pinnacleChart" or "arrowChart").
Key type modifiers used with USER charts:
| Modifier | Role |
|---|---|
DIFFERENCE |
computes value[1] − value[0] from a "a|b" binding |
NONEGATIVE |
render flag — suppresses drawing the chart symbol where the computed value ≤ 0 (data row is still processed; only rendering is skipped) |
RELOCATE |
relocates the chart symbol to the geometry centroid |
BOX |
adds a background box behind the label |
BOTTOMTITLE |
places title below the chart symbol |
NOLEGEND |
excludes this layer from the map legend |
Split-winner pattern — two layers from one dataset, no pre-filtering needed:
// Layer A — shows only communes where Sì wins (voti_si − voti_no > 0)
myMap.layer("comuni")
.data({ url: DATA_URL, type: "csv" })
.binding({ lookup: "cod_istat", value: "voti_no|voti_si", title: "desc_com" })
.type("CHART|USER|3D|DIFFERENCE|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|BOX|BOTTOMTITLE|NOLEGEND")
.style({
name: "chart_si",
userdraw: "pinnacleChart",
colorscheme: SI_COLORS,
sizepow: 2,
normalsizevalue: 1000000,
aggregationfield: "desc_com",
titlefield: "desc_com",
datafields: ["desc_com","desc_prov","margin_f"],
showdata: "true"
})
.meta({ name: "chart_si", tooltip: "{{desc_com}}<br>voti in più Sì: {{raw.margin_f}}" })
.define();
// Layer B — shows only communes where No wins: swap binding order, NONEGATIVE drops the rest
myMap.layer("comuni")
.data({ url: DATA_URL, type: "csv" })
.binding({ lookup: "cod_istat", value: "voti_si|voti_no", title: "desc_com" }) // ← swapped
.type("CHART|USER|3D|DIFFERENCE|HEADTAIL|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|NOLEGEND")
.style({ name: "chart_no", userdraw: "pinnacleChart", /* ... */ showdata: "true" })
.meta({ name: "chart_no", tooltip: "..." })
.define();
Binding order determines sign:
"a|b"→b − a. WithNONEGATIVE, only locations where the result > 0 get a chart drawn. Swappingaandbbetween two layers gives "A wins" vs "B wins" without any data pre-processing.{{raw.fieldname}}in tooltip accesses fields listed indatafields— useful for pre-formatted strings (e.g."12.345"from.toLocaleString()).
Invisible point anchor layer — load centroid geometry without rendering anything:
// Required when CHART|USER layers need to snap to precise urban centroids
// For POINT geometry, fillopacity:0 alone still renders a dot — scale:0 suppresses it completely
myMap.layer("centroids")
.data({ url: CENTROIDS_URL, type: "geojson" })
.binding({ geo: "geometry", id: "PRO_COM", title: "PRO_COM" })
.type("FEATURE|NOLEGEND")
.style({
colorscheme: ["none"],
scale: 0, // ← required for point geometry
fillopacity: 0,
linecolor: "none",
linewidth: 0,
showdata: "true"
})
.define();
Diverging scales, density patterns, road-tracing, SEQUENCE charts → API_REFERENCE.md § Special Cases Complete working examples → EXAMPLES.md Data preprocessing (data.js) → DATA_JS_GUIDE.md Symbols/icons → SYMBOLS_GUIDE.md Troubleshooting → TROUBLESHOOTING.md
Facet Sidebar (filter panel updated on zoom/pan)
A facet sidebar lets users filter the map by clicking category values or dragging range sliders. It auto-updates on every zoom, pan, and filter change. This pattern requires three CDN plugins:
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/plugins/format.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/plugins/facet.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@master/plugins/show_facets.js"></script>
Sidebar HTML structure
Use the canonical template below. The counter-chips pattern (🌳 N totali / 👁 N in vista) gives users immediate feedback on dataset size and current view. Adapt the emoji and label text to the subject matter.
<!-- Sidebar — place outside #map_div, position with CSS (right panel, overlay, etc.) -->
<div id="sidebar_div" style="
position:absolute; top:0; right:0; width:320px; height:100%;
display:flex; flex-direction:column;
background:rgba(248,247,242,0.97); box-shadow:-2px 0 8px rgba(0,0,0,0.08);
font-family:sans-serif; z-index:900;">
<!-- Title block -->
<div style="padding:1.2em 1.2em 0.6em">
<div style="font-size:1.3em; font-weight:700">🌳 Map Title</div>
<div style="font-size:0.85em; color:#888; margin-top:0.2em">Subtitle / data source line</div>
</div>
<!-- Counter chips: total + in-view -->
<div style="padding:0 1.2em 0.8em; display:flex; gap:0.5em; flex-wrap:wrap; border-bottom:1px solid #e0dfd8">
<span id="count-chip-total" style="
background:#fff; border:1.5px solid #ccc; border-radius:2em;
padding:0.3em 0.9em; font-size:0.9em; white-space:nowrap">
🌳 <b id="count-total">—</b> totali
</span>
<span id="count-chip-visible" style="
background:#fff; border:1.5px solid #ccc; border-radius:2em;
padding:0.3em 0.9em; font-size:0.9em; white-space:nowrap">
👁 <b id="count-visible">—</b> in vista
</span>
</div>
<!-- Active-filter banner (hidden until a filter is applied) -->
<div id="filter-div" style="display:none; padding:0.4em 1.2em; background:#fff3cd; font-size:0.82em">
<b>Filtro attivo:</b> <span id="filter" style="font-style:italic"></span>
<button onclick="clearFilter()" style="
float:right; background:none; border:none; cursor:pointer;
font-size:1em; color:#666">✕</button>
</div>
<!-- Data source credit -->
<div style="padding:0.4em 1.2em; font-size:0.78em; color:#999; border-bottom:1px solid #e0dfd8">
Dati: <a href="#" target="_blank" style="color:#888">Source Name</a> (CC-BY)
</div>
<!-- Scrollable facet area -->
<div style="overflow-y:auto; flex:1; padding:0 0.4em">
<div id="show-facets-div"></div>
</div>
</div>
Set the total count once after data loads (not on every draw):
document.getElementById("count-total").textContent = DATA.length;
ixmaps.statistics — the facet engine hook
Override ixmaps.statistics to compute and render facets. It is called by ixmaps after every draw:
ixmaps.statistics = function (szId) {
var themeObj = ixmaps.getThemeObj(szId);
if (!themeObj) return;
var lastFilter = themeObj.szFilter || "";
// Fields to facet — order determines sidebar order.
// NONUMERIC flag: suppresses numeric range sliders for fields that
// have many unique numbers but are better treated as categories.
// Fields with >N unique values auto-render as text-search inputs.
ixmaps.data.fShowFacetValues = false;
var szFieldsA = [
"CATEGORY_FIELD", // categorical — picks up theme colors if it's the value field
"HEIGHT_CLASS", // ordinal text
"STREET_NAME", // high-cardinality → auto text-search input
"YEAR" // numeric range slider (omit NONUMERIC to allow)
];
var facetsA = ixmaps.data.getFacets(
lastFilter, "user_legend", szFieldsA, szId, "map", "NONUMERIC"
);
if (facetsA && facetsA.length) {
ixmaps.data.showFacets(lastFilter, "show-facets-div", facetsA);
}
// update visible-count chip — use your inline DATA array, not theme.indexA
myMap.then(function(m) {
var bounds = m.getBounds();
if (!bounds || bounds.length !== 4) return;
var swLat = bounds[0], swLng = bounds[1], neLat = bounds[2], neLng = bounds[3];
var vis = 0;
DATA.forEach(function(d) {
if (d.lat >= swLat && d.lat <= neLat && d.lon >= swLng && d.lon <= neLng) vis++;
});
var el = document.getElementById("count-visible");
if (el) el.textContent = vis;
});
};
React to layer draw — map.on("layerdraw")
Use the event API (preferred over the legacy htmlgui_onDrawTheme hook):
myMap.on("layerdraw", function(e) {
var themeObj = ixmaps.getThemeObj(e.id);
// skip helper/invisible layers
if (!themeObj) return;
if (themeObj.szFlag && themeObj.szFlag.match(/NOLEGEND/)) return;
if (!themeObj.fVisible) return;
ixmaps.statistics(e.id);
// show/hide active-filter banner
if (themeObj.szFilter) {
document.getElementById("filter").innerHTML = themeObj.szFilter;
document.getElementById("filter-div").style.display = "";
} else {
document.getElementById("filter-div").style.display = "none";
}
});
Legacy hook
ixmaps.htmlgui_onDrawTheme— still works but is old-style. Prefermap.on("layerdraw")for new code. If an existing plugin already useshtmlgui_onDrawTheme, wrap it (var _prev = ixmaps.htmlgui_onDrawTheme; ixmaps.htmlgui_onDrawTheme = function(szId){ ...; _prev && _prev(szId); }) rather than overwriting.
Clear all facet filters
function clearFilter() {
ixmaps.data.facetsFilterA = [];
myMap.then(function(m) {
m.changeThemeStyle("yourThemeName", "filter", "remove");
});
}
Facet button style overrides
show_facets.js generates .btn-primary buttons and .badge count labels. Override them to match your design:
#show-facets-div .btn-primary {
background-color: #fff;
color: #334;
border: none;
border-bottom: solid rgba(128,128,128,0.25) 1px;
border-radius: 0;
}
#show-facets-div .btn-primary:hover,
#show-facets-div .btn-primary:focus {
background-color: #f0efe8;
color: #112;
outline: none; box-shadow: none;
}
#show-facets-div .badge {
background: transparent;
color: #778;
font-size: 13px;
font-weight: 400;
}
NONUMERIC flag
Pass "NONUMERIC" as the last argument to getFacets to suppress range-slider facets for fields that happen to contain numbers but are really categories (e.g. year codes, ID numbers). Without this flag, any numeric field will render as a histogram + dual-handle slider.
Category field gets theme colors automatically
If one of the facet fields matches the theme's value binding field, show_facets.js automatically colors each facet button with the corresponding theme color. No extra config needed — just include the field name.
theme.szFilter vs themeObj.szFilter
lastFilter = themeObj.szFilter || "" — always read the filter from the theme object, not a local variable. The facet engine updates it internally; reading it fresh on each draw ensures facets reflect the current filter state.
Overlay Indicator Layer (small dot on top of main bubble)
Use a second layer over the main bubbles to show a per-item status flag — e.g. failure risk class, alert state, certification level — without changing the primary color scheme.
Pattern
-
Add a constant
_dotfield to source data so the size binding has a numeric value:DATA.forEach(function(d) { d._dot = 50; }); -
Filter to only the items worth showing (e.g. only elevated/extreme risk, skip negligible):
var riskData = DATA.filter(function(d) { return d.RISK && d.RISK.match(/^(HIGH|EXTREME)/); }); -
Define the overlay layer using
CATEGORICAL|NOLEGENDpiped into the type string,scalefor size, and_dotfor the size binding:var indicatorTheme = ixmaps.layer("risk_dots") .data({ obj: riskData, type: "json" }) .binding({ geo: "lat|lon", value: "RISK", title: "NAME", size: "_dot" }) .type("CHART|BUBBLE|CATEGORICAL|NOLEGEND") .style({ colorscheme: ["#ff9800", "#d32f2f"], values: ["HIGH", "EXTREME"], normalsizevalue: "1000", // same as main layer scale: 0.1, // 10% of main bubble size → small indicator dot fillopacity: 1.0, strokewidth: "0", showdata: "true", align: "bottom" // anchor dot to bottom of main bubble }) .meta({ name: "risk_dots" }) .title("Risk indicator") .define(); myMap.layer(indicatorTheme, "direct");
Key rules
| Rule | Why |
|---|---|
NOLEGEND must be piped into the type string: `"CHART |
BUBBLE |
Use scale: 0.1 rather than a very large normalsizevalue |
scale is a clean multiplier applied after size calculation; normalsizevalue only works cleanly when the size field has a known typical range |
Keep normalsizevalue the same as the main layer |
Makes the dot size proportional to the main bubble for the same item — a bigger tree gets a bigger dot |
Add _dot constant before filtering |
DATA.forEach(d => d._dot = 50) on the source array means every filtered subset inherits the field |
| Filter to only meaningful states | Empty dots for the "all-clear" state (A/negligible) add clutter without information |
Skip the indicator layer in layerdraw
The draw handler must skip NOLEGEND layers to avoid running ixmaps.statistics on the indicator layer (which would show the risk categories as the main facets). The guard is already in the recommended pattern above:
if (themeObj.szFlag && themeObj.szFlag.match(/NOLEGEND/)) return;
CSS Conflicts with External Frameworks (Bootstrap etc.)
Never load Bootstrap 3 (or similar CSS frameworks) alongside ixmaps. Bootstrap 3's .hidden { display:none !important } rule silently breaks ixmaps UI elements — toolbar buttons, tooltip, and context menu all become invisible because:
- ixmaps creates elements with
class="hidden"and controls visibility viaelement.style.display = "flex/block/inline" - Bootstrap's
!importanton.hiddenbeats inline styles — ixmaps can never win - The failure is silent: no JS errors, elements just stay invisible
Root fix: standalone facet CSS
Instead of Bootstrap, include ~35 lines of standalone CSS that covers only what show_facets.js generates:
/* ── Standalone facet CSS (replaces Bootstrap 3) ── */
.list-group { padding-left: 0; margin-bottom: 20px; list-style: none; }
.facet, .facet-active { margin-bottom: 0; }
/* CRITICAL: must use display:table, NOT flexbox.
show_facets.js renders a colored proportion bar as a <div> immediately
after each <button> inside .input-group. With display:table, they stack
vertically (each becomes a table row). With display:flex, the bar becomes
a horizontal sibling and disappears entirely. */
.input-group { position: relative; display: table; border-collapse: separate; width: 100%; }
.input-group .form-control { display: table-cell; width: 100%; }
.input-group-btn { display: table-cell; white-space: nowrap; width: 1%; vertical-align: middle; }
.form-control {
display: block; width: 100%;
padding: 4px 8px; font-size: 14px; line-height: 1.43;
color: #555; background: #fff;
border: 1px solid #ccc; border-radius: 4px;
}
.form-control:focus { outline: none; border-color: #66afe9; }
.btn {
display: inline-block; padding: 5px 10px;
font-size: 14px; font-weight: 400; line-height: 1.43;
text-align: center; white-space: nowrap; vertical-align: middle;
cursor: pointer; border: 1px solid transparent; border-radius: 4px;
background: none; font-family: inherit;
}
.btn-block { display: block; width: 100%; }
.btn-primary { color: #fff; background: #337ab7; border-color: #2e6da4; }
.btn-default { color: #333; background: #fff; border-color: #ccc; }
.btn-default:hover { background: #e6e6e6; border-color: #adadad; }
.badge {
display: inline-block; min-width: 10px; padding: 3px 7px;
font-size: 12px; font-weight: 700; line-height: 1;
color: #fff; text-align: center; white-space: nowrap;
vertical-align: baseline; background: #777; border-radius: 10px;
}
.pull-right { float: right !important; }
Tooltip and context menu fix
ixmaps creates #tooltip and #contextmenu with class="hidden visibility-hidden;" (note: literal semicolon in the class attribute). Always add this safety fix in myMap.then():
myMap.then(function() {
setTimeout(function() {
["tooltip","contextmenu"].forEach(function(id) {
var el = document.getElementById(id);
if (el) {
el.classList.remove("hidden");
el.classList.remove("visibility-hidden");
el.style.display = "none";
}
});
}, 500);
});
Fallback: CSS attribute-selector workaround
If Bootstrap cannot be removed (e.g. it is required by other page content), use higher-specificity rules to override the conflict. Specificity (0,2,0) beats Bootstrap's (0,1,0):
.hidden[style*="display: flex"], .hidden[style*="display:flex"] { display: flex !important; }
.hidden[style*="display: block"], .hidden[style*="display:block"] { display: block !important; }
.hidden[style*="display: inline"], .hidden[style*="display:inline"] { display: inline !important; }
Note: CDN-loaded Bootstrap stylesheets are CORS-blocked, so JS-based patching of
cssRulesdoes not work. The CSS or JS approaches above are the only reliable fixes.