User Charts
iXMaps chart symbols are not limited to the built-in shapes. The userdraw style property is a general plugin API for drawing arbitrary SVG-based graphics as map charts: any JavaScript function that produces SVG can act as the renderer for a layer’s symbols.
The plugin principle
The API rests on a strict division of labour:
iXMaps does everything around the drawing — loading and filtering the data, aggregating values, positioning the chart (lat/lon anchor, geometry centroid with RELOCATE, or grid cell), computing its size through the regular sizing system (normalsizevalue, sizepow, zoom scaling), resolving the color class from colorscheme, and integrating legend and tooltips.
The plugin does one thing: draw its SVG code into the SVG target that iXMaps hands it (args.target, a prepared <g> group). It reads the precomputed value, size and color from args and produces shapes — nothing else. That is why plugin code stays small: each of the shipped plugins is essentially one SVG path plus an optional label.
SVG library — your choice
The three shipped plugins draw with D3 (v3), which is comfortable for SVG generation — but D3 is not mandatory. The plugin contract is only “put SVG into args.target”; any technique works, including native DOM:
ixmaps.squareChart = function (SVGDocument, args) {
var size = args.size || 50;
var rect = SVGDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", -size / 2);
rect.setAttribute("y", -size / 2);
rect.setAttribute("width", size);
rect.setAttribute("height", size);
rect.setAttribute("fill", args.theme.colorScheme[args.class]);
args.target.appendChild(rect);
};The D3 <script> include shown below is required by the three shipped plugins; a plugin of your own only needs whatever it uses itself.
The shipped plugins
Three ready-made chart functions ship with the framework:
| Function | Shape | Script |
|---|---|---|
pinnacleChart |
Vertical pillar/bar — height encodes the value | usercharts/d3/chart.js |
arrowChart |
Up-arrow for positive values, down-arrow for negative ones | usercharts/d3/arrow_chart.js |
lolliChart |
Lollipop pin — a circle on a stem, height encodes the value | usercharts/d3/lolli_chart.js |
All three share the same sizing logic (normalsizevalue, rangescale) and read their color from the layer’s colorscheme class; GRADIENT in the type string switches the fill to a vertical gradient, and NONEGATIVE suppresses negative values.
How custom chart functions work
You write a JavaScript function, attach it to the ixmaps namespace, then reference it by name in .style({ userdraw: "functionName" }). iXMaps calls your function once per rendered feature, passing the SVG context and data.
Function signature:
ixmaps.myChart = function(SVGDocument, args) {
// args properties:
// args.target — the SVG group element to draw into
// args.value — the primary data value for this feature
// args.values — array of all values (for multi-value bindings)
// args.theme — theme object (contains style, color scheme, etc.)
// args.item — the raw data item/row
// args.dbRecord — the full underlying database record
// args.size — this item's computed radius/size
// args.maxSize — the theme's maximum radius/size
// args.color — the resolved fill color
// args.textcolor — the resolved text color
// args.ccolor — chart-derived colors (from the theme's color scheme)
// args.class — color class index (maps to colorscheme)
// args.flag — the raw type-string ("flag") of the theme
// args.mark — an internal per-item marker value
var svg = d3.select(args.target);
// ... draw with D3 or SVG DOM methods ...
return { x: 0, y: 0 }; // offset used to reposition the chart group — a falsy return counts as a draw error
};Two optional companion functions, named by suffixing your userdraw name, run once before/after the whole per-feature loop rather than once per feature — useful for setup/teardown (e.g. building a shared <defs> block):
ixmaps.myChart_init = function(SVGDocument, args) { /* args = { target, theme } — runs once before */ };
ixmaps.myChart_finish = function(SVGDocument, args) { /* args = { target, theme } — runs once after */ };userdraw vs chartdraw
The current flat API uses userdraw as the style property name. Older iXMaps documentation and the legacy API used chartdraw — this name is no longer valid with the flat API.
Required scripts (shipped plugins)
For the three shipped plugins, D3 plus the script of each chart function you use are required in addition to ixmaps.js — a plugin of your own needs neither unless it draws with D3 itself:
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@1/ixmaps.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@1/usercharts/d3/chart.js"></script> <!-- pinnacleChart -->
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@1/usercharts/d3/arrow_chart.js"></script> <!-- arrowChart -->
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@1/usercharts/d3/lolli_chart.js"></script> <!-- lolliChart -->Script load order relative to ixmaps.js does not matter.
Type flags
CHART|USER layers use several modifiers not found on other types:
| Modifier | Role |
|---|---|
DIFFERENCE |
Computes value[1] − value[0] from a "a\|b" binding |
NONEGATIVE |
Suppresses rendering where computed value ≤ 0 (data still processed) |
RELOCATE |
With AGGREGATE: recomputes the merged position as the average of the original points that were combined |
BOX |
Adds a background box behind the label |
BOTTOMTITLE |
Places title below the chart symbol |
NOLEGEND |
Excludes this layer from the map legend |
pinnacleChart
A vertical pillar/bar chart rendered at each feature location. Size encodes a numeric value.
myMap.layer("comuni")
.data({ url: DATA_URL, type: "csv" })
.binding({ lookup: "cod_istat", value: "value", title: "name" })
.type("CHART|USER|3D|AGGREGATE|RECT|RELOCATE|SUM|VALUES|BOX|BOTTOMTITLE|NOLEGEND")
.style({
userdraw: "pinnacleChart",
colorscheme: ["#0066cc","#004499"],
normalsizevalue: 1000000,
sizepow: 2,
aggregationfield: "name",
titlefield: "name",
showdata: "true"
})
.meta({ name: "pinnacles", tooltip: "{{name}}: {{value}}" })
.define();Split-winner pattern
Two layers from the same dataset — one for each “winner” — without pre-filtering. The DIFFERENCE modifier computes b − a; NONEGATIVE skips locations where the difference is ≤ 0.
// Layer A — shows locations where "sì" > "no"
myMap.layer("comuni")
.data({ url: DATA_URL, type: "csv" })
.binding({ lookup: "cod_istat", value: "voti_no|voti_si", title: "comune" })
.type("CHART|USER|3D|DIFFERENCE|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|BOX|BOTTOMTITLE|NOLEGEND")
.style({
userdraw: "pinnacleChart",
colorscheme: ["#2e7d32","#4caf50"],
normalsizevalue: 500000,
sizepow: 2,
aggregationfield: "comune",
titlefield: "comune",
showdata: "true"
})
.meta({ name: "si_wins", tooltip: "{{comune}}: Sì wins by {{$value}}" })
.define();
// Layer B — shows locations where "no" > "sì" (binding order swapped)
myMap.layer("comuni")
.data({ url: DATA_URL, type: "csv" })
.binding({ lookup: "cod_istat", value: "voti_si|voti_no", title: "comune" })
.type("CHART|USER|3D|DIFFERENCE|AGGREGATE|RECT|RELOCATE|SUM|VALUES|NONEGATIVE|BOX|BOTTOMTITLE|NOLEGEND")
.style({
userdraw: "pinnacleChart",
colorscheme: ["#b71c1c","#ef5350"],
normalsizevalue: 500000,
sizepow: 2,
aggregationfield: "comune",
titlefield: "comune",
showdata: "true"
})
.meta({ name: "no_wins", tooltip: "{{comune}}: No wins by {{$value}}" })
.define();Binding order determines sign: "a|b" computes b − a. Swapping a and b between two layers gives “A wins” vs “B wins” without any data preprocessing.
Invisible centroid anchor layer
When CHART|USER layers need to snap to precise centroids (not grid positions), load a centroid geometry layer first without rendering it visually:
myMap.layer("centroids")
.data({ url: CENTROIDS_GEOJSON_URL, type: "geojson" })
.binding({ geo: "geometry", id: "PRO_COM", title: "PRO_COM" })
.type("FEATURE|NOLEGEND")
.style({
colorscheme: ["none"],
scale: 0, // scale:0 fully suppresses rendering for point geometry
fillopacity: 0,
linecolor: "none",
linewidth: 0,
showdata: "true"
})
.define();For point geometry, fillopacity: 0 alone still renders a tiny dot. scale: 0 suppresses the draw entirely.