User Charts

Draw custom chart symbols in iXMaps with userdraw and chartdraw functions: required scripts, type flags and patterns like pinnacleChart.

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 */ };
Warninguserdraw 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.