# 02viz — Geospatial Visualization Studio

A multi-engine, fully-offline chart, diagram, and label studio inside QGIS — from raw layer attributes to interactive, publication-grade visuals that stay linked to the map.

v0.15.13 17 chart types 4 rendering engines 3 studio tabs QGIS 3.28 – 4.99 GPL-3.0

## Overview & Philosophy

02viz turns the QGIS environment into a full data-visualisation studio. Charting in GIS has historically meant exporting an attribute table to a spreadsheet or wrestling with a single fixed plotting library; 02viz removes that friction by putting one dock — three tabs — Charts, Map Diagrams, and Labels — directly onto the canvas, all sharing a single layer selector. The result is a visual that is not a screenshot, but a live artefact that remains bound to the layer it describes.

17
Chart Types
4
Rendering Engines
6
Animatable Charts
5
Reference Overlays
8
Colour Palettes

Three engines are vendored into the plugin and run with no network, no account, and no external Python dependency; a fourth — matplotlib / seaborn — is optional and auto-detected. All statistics and data shaping are pure Python, so the studio works identically offline on QGIS 3.28 through the QGIS 4 line (Qt5 and Qt6).

Design principle. 02viz separates data shaping from rendering. One typed spec — field bindings, aggregation, bins, palettes, overlays — is assembled in `core/` and then reinterpreted by whichever engine the user picks. Every engine exposes only `build_html(spec) → str` and never touches Qt, which is why the same chart reads identically in ECharts, Plotly, Vega-Lite, and matplotlib, and why the studio is batch-exportable and testable in pure Python.

## Quick Start

STEP 1
Open the dock
02viz Studio toolbar button
→
STEP 2
Bind a layer
vector layer or external CSV/XLSX/ODS/GPKG
→
STEP 3
Choose engine + type
or press 💡 Suggest
→
STEP 4
Render chart
interactive, click-to-select
→
STEP 5
Export / style
one-file HTML, PNG, diagrams, labels

New to it all? Press ❔ Guide for a self-contained, illustrated walkthrough of every feature, or press ✨ Explore to have 02viz profile the whole layer into a dashboard in one click.

## How the Studio Works

02viz follows Wilkinson's (2005) Grammar of Graphics: a statistical graphic is decomposed into independent components — data, aesthetic mappings, geometric objects, scales, and transformations. The spec-assembly layer (`core/transform.py`, `core/stats.py`) produces a typed `data` dictionary; each engine re-expresses that one structure through its own primitives. This separation is the single most important architectural idea in the plugin — change the engine, and the data story does not change.

The selection bridge (chart clicks → map selection, and map selection → dimmed chart) works through the browser page `document.title` rather than a JavaScript window object, because `addToJavaScriptWindowObject` crashes inside WebCore on Qt 5.15's QtWebKit fork and `QWebChannel` exists only on WebEngine. Title transport is the one mechanism safe and identical on every QGIS web stack; in a plain browser it is a harmless title blip.

GROUP 1

### Rendering Engines

⚡

#### Apache ECharts

engines/echarts.py · 567 lines · ~1.0 MB vendored
The default engine and the most compatible: ECharts (Li et al., 2018) is a declarative framework that renders all 17 chart types with full ES5 compatibility, so it is the only engine guaranteed to work on QGIS builds using Qt 5.15's QtWebKit fork. It drives the Explore dashboard and the animation timeline.

##### Capabilities

Dimension | Detail
--- | ---
Chart types | All 17
Animation | `timeline` component — autoPlay, loop, stable axes
Reference overlays | `markLine` / `markArea`
Special rendering | custom `renderItem` for violin polygons and error whiskers
WebKit / ES5 | Yes — safe on QtWebKit fork

Why default. Plotly.js and Vega-Lite ship ES6+ bundles that QtWebKit cannot parse, leaving a blank panel. ECharts's ES5 build is the only fully-offline option that renders identically on both the modern WebEngine and the legacy WebKit fork — so 02viz defaults to it unless you specifically choose another engine.
📈

#### Plotly.js

engines/plotly.py · 465 lines · ~4.5 MB vendored
Plotly.js renders all 17 chart types and provides the richest animation controls — a frames-plus-slider model with explicit play/pause `updatemenus`, ideal for Gapminder-style bubble races and exploratory temporal playback. Because its bundle is ES6+, it requires WebEngine (or the external browser fallback), not WebKit.

##### Capabilities

Dimension | Detail
--- | ---
Chart types | All 17
Animation | frames + slider + play/pause `updatemenus`
Reference overlays | `layout.shapes` (lines / rects) + `layout.annotations`
Cross-filter | `Plotly.restyle()` with per-trace `selected`/`unselected` opacity
WebKit / ES5 | No — shows a styled explainer page instead of a blank panel

Fallback behaviour. On WebKit-only builds, 02viz does not fail silently: it renders an explainer page telling the user why the panel is blank and offering three options — switch to ECharts, export to HTML, or open in the system browser.
🧩

#### Vega-Lite

engines/vegalite.py · 572 lines · ~0.76 MB vendored
Vega-Lite (Satyanarayan et al., 2017) is the direct declarative realisation of the Grammar of Graphics: a JSON specification language compiled to Vega at runtime via `vegaLite.compile()`. It supports 14 of 17 chart types — treemap, sunburst, and radar are excluded because hierarchical and radial layouts are outside the Vega-Lite grammar. Users may edit the generated JSON directly, with validation before rendering.

##### Capabilities

Dimension | Detail
--- | ---
Chart types | 14 — no treemap, sunburst, or radar
Animation | None
Data injection | feature rows with IDs injected as named dataset `o2viz`
Spec editing | custom JSON editor with validation
Violin workaround | ranged-area mark, re-paired polygon half-points (avoids `order` channel collapse)

🎨

#### Matplotlib / seaborn OPTIONAL

engines/mpl.py · 275 lines · auto-detected
The only engine with optional Python dependencies. It imports matplotlib and seaborn lazily, uses `matplotlib.use("Agg")` for headless rendering, applies `seaborn.set_theme(style="whitegrid")` when available, and produces a static 8.2″ × 5.0″ figure at 150 DPI, base64-encoded as PNG. It is the engine of choice when the goal is a fixed, print-ready figure rather than an interactive one.

The same spec rendered by the optional matplotlib / seaborn engine as a publication-grade static figure.

##### Capabilities

Dimension | Detail
--- | ---
Chart types | 11 — bar, line, area, scatter, bubble, histogram, pie, box, heatmap, density, violin
Output | 8.2″ × 5.0″ @ 150 DPI, base64 PNG
Interaction | None (static); bridge calls stubbed inert
Dependency | Auto-detected at startup via `core/requirements.py` — never pip-installed for you

Graceful absence. When matplotlib is missing, 02viz shows the exact install command for you to run yourself; the core studio remains dependency-free and fully functional with the three vendored JS engines.
GROUP 2

### Chart Types (17)

Every chart type is documented as a self-contained card: a rendered screenshot, a real-data example (real İzmir / Türkiye figures with the source cited), when to use it, what data it needs, how to read it, and how to interpret it and where it applies — followed by the exact field bindings and engine support.

📊

#### Bar Chart

chart key: bar · animatable · overlay-eligible
The workhorse of quantitative comparison. A bar chart encodes a numeric value as position along a common scale — the single most accurately-perceived visual channel in Cleveland & McGill's (1984) ranking of graphical perception.

İzmir district population (2023) — one bar per district, height encodes population on a common axis.

Real data. Buca 523,487 · Karabağlar 476,500 · Bornova 447,553 · Karşıyaka 341,857 · Konak 327,300 · Bayraklı 300,949 · Çiğli 215,172 · Torbalı 214,059. Source: TÜİK ADNKS 2023.

##### When to use

Whenever the question is "which category is larger, and by how much": comparing one numeric measure across districts, land-use classes, or periods. Prefer it over a pie chart once categories exceed a handful. Use grouped bars (Group field) to add a second categorical dimension, and stacked bars (checkbox) to show additive composition.

##### What data you need

A categorical field for X and a numeric field for Y — or just the category plus a count aggregation. A Group field optionally splits into coloured series; an aggregation (sum / mean / …) is applied per category.

##### How to read it

Compare the tops of the bars on the shared axis — the distance between two bar tops is the difference in value. In grouped bars, compare within a cluster and across clusters; in stacked bars, the total is the top edge and each segment is a part.

##### Interpretation & applications

Spot the largest and smallest category, dominance, or balance. In planning: population by district, budget by department, area by land-use class. Always check the axis origin — a non-zero baseline exaggerates differences, so read the values, not just the visual gap.

##### Field bindings & controls

Binding | Role | Notes
--- | --- | ---
X | Category field | Categorical (or numeric binned)
Y | Value field | Numeric; aggregated with the chosen aggregate
Group | Series field | Splits into coloured series; enables stacked bars
Aggregate | count · sum · mean · median · min · max | Applied per category
Top-N / Sort | N + order | Collapses remainder into "Other"

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — animation (play axis) and reference overlays (mean / median / ±1σ / IQR / target) both available.

See also: Line for ordered trends · Histogram for the distribution of a single numeric field · Reference overlays to annotate the value axis.
📈

#### Line Chart

chart key: line · animatable · overlay-eligible
A line chart connects ordered values to reveal trend and trajectory. The connective line encodes change between successive points, making it the natural choice when the X axis has an intrinsic order — time, distance, a ranked sequence, or a planning horizon.

Türkiye population 2015–2024 — slope between points encodes the rate of change.

Real data. 78.22 million (2015) rising to 85.52 million (2024); annual growth slows from ~1.3% to ~0.2%. Source: TÜİK / World Bank.

##### When to use

When the X axis is ordered — time, distance, phases — and you want to show change over that order. Pair with Animate by to unfold the trajectory over time, or with a Group field to overlay multiple series on one axis. Bars suit unordered categories; lines suit sequences.

##### What data you need

An ordered X field (year, distance, rank) and a numeric Y field. A Group field produces one polyline per category; an aggregation is applied per (X, group) pair.

##### How to read it

Follow the slope — a steep rise is fast growth, a steep fall is rapid decline, a flat run is stability. Peaks and troughs mark turning points. With multiple lines, compare their trajectories and where they cross.

##### Interpretation & applications

Identify growth or decline, inflection points, and diverging series. In planning: population, housing starts, or land consumption over a plan horizon — and, animated, the evolving trajectory of each district.

##### Field bindings & controls

Binding | Role
--- | ---
X | Ordered category / numeric (time, sequence)
Y | Numeric
Group | One polyline per category (optional)
Aggregate / Top-N / Sort | Applied per (X, group) pair

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — animatable; reference overlays available.

See also: Area for a filled variant · Animation for the play axis · Overlays to mark a target on the trend.
🏞️

#### Area Chart

chart key: area · animatable · overlay-eligible
A line chart with the region under the curve filled. The fill adds a volume reading — useful for emphasising magnitude over time or, when stacked, the changing composition of a whole across categories.

Türkiye age structure 2015–2024 (stacked) — band heights show how age groups shift.

Real data. The 0–14 share falls 23.99% → 20.93% while the 65+ share rises 8.25% → 10.64% — a visibly ageing population. Source: TÜİK ADNKS.

##### When to use

Ordered X with a magnitude story. Stacked areas show how parts of a total evolve together (composition over time); unstacked areas compare magnitudes directly, overlapping where one exceeds another. The Stacked checkbox toggles between the two.

##### What data you need

An ordered X field, a numeric Y field, and (for the composition reading) a Group field that splits the total into coloured bands.

##### How to read it

In a stacked area the top edge is the total and each band's height is its share of that total; watch for bands widening or narrowing over time. In an unstacked area, compare the filled silhouettes' heights.

##### Interpretation & applications

Read structural shift: a band that grows while another shrinks is a reallocation. In planning: land-use or energy-mix change over a plan period, showing whether a city is densifying, sprawling, or greening.

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — animatable (composition over years); reference overlays available.

See also: Line · Bar (stacked) for categorical composition.
⚫

#### Scatter Plot

chart key: scatter · animatable · overlay-eligible
The fundamental bivariate display: two numeric fields plotted against each other, one point per feature. It exposes association, clusters, gaps, and outliers that summary statistics conceal. It is the first chart the Smart Assistant proposes when two numeric fields correlate.

İzmir districts: population vs area, with a least-squares trend line.

Real data. Nine districts with both figures — e.g. Buca 523.5k / 181.9 km², Karşıyaka 341.9k / 72.3 km², Menemen 207.7k / 612.4 km². Source: TÜİK ADNKS 2023; areas from İzmir-MOD.

##### When to use

When you have two numeric fields and want to know whether they are related — the first look at any suspected association. The optional trend line overlays a least-squares regression; Group colours points by category to expose structure within the cloud.

##### What data you need

Two numeric fields (X and Y), one point per feature. A Group field is optional for colouring; the trend checkbox adds the regression line.

##### How to read it

Read four properties: direction (upward = positive association, downward = negative), form (linear or curved), strength (tight cloud = strong, diffuse = weak), and outliers (isolated points far from the cloud).

##### Interpretation & applications

Ask whether X predicts Y and where the exceptions are. In planning: population vs density (sprawl check), income vs education, distance-to-transit vs property value. The trend line's slope and the Pearson \(r\) (available in the Explore dashboard) quantify the relationship.

##### Method — least-squares trend line

\(\beta = \dfrac{S_{xy}}{S_{xx}}, \quad \alpha = \bar{y} - \beta \bar{x}\)
where \(S_{xy} = \sum (x_i-\bar{x})(y_i-\bar{y})\) and \(S_{xx} = \sum (x_i-\bar{x})^2\).

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — animatable (Gapminder-style); reference overlays available.

See also: Bubble for a third dimension · Correlation for the Pearson \(r\) · Explore dashboard for the strongest-relationship scatter.
🫧

#### Bubble Chart

chart key: bubble · animatable · overlay-eligible
A scatter plot with a third numeric dimension encoded as area. Because the eye reads area — not radius — a naive linear radius scaling systematically understates large values; 02viz therefore scales radii by square root so that perceived area is proportional to value.

İzmir districts: area vs density — bubble size is population.

Real data. Karşıyaka 72.3 km² / 4,728 ppl/km² versus Menemen 612.4 km² / 339 ppl/km²; bubble area = population. Source: TÜİK ADNKS 2023; areas from İzmir-MOD.

##### When to use

When you have three numeric fields: two for position and one for magnitude. It is the classic way to show which entities are both extreme on two measures and large on a third — and it animates into a Gapminder-style bubble race.

##### What data you need

Two numeric fields (X, Y) plus a numeric Value field for bubble area; Group is optional for colour.

##### How to read it

Compare bubble areas, never diameters — a bubble twice the diameter is four times the value. Look for the largest bubbles and where they sit on the two axes; during animation, a fixed value keeps a fixed size across frames.

##### Interpretation & applications

Identify the dominant entities and any trade-off between the three variables. In planning: GDP vs population with emissions as size, or district budget vs area with population as size.

##### Method — square-root radius scaling

\(r = r_{\min} + (r_{\max} - r_{\min}) \sqrt{\dfrac{v - v_{\min}}{v_{\max} - v_{\min}}}\)

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — animatable; reference overlays available.

See also: Scatter · Animation for the Gapminder-style bubble race.
📶

#### Histogram

chart key: histogram
The canonical display of a single numeric field's distribution: value on the X axis, frequency on the Y. It reveals centre, spread, skew, and modality at a glance, and is the natural first look at any unfamiliar numeric attribute.

İzmir district populations — the shape of 30 districts' population distribution.

Real data. Thirty districts from Beydağ 12,094 up to Buca 523,487 — a heavily right-skewed distribution. Source: TÜİK ADNKS 2023.

##### When to use

Whenever you meet an unfamiliar numeric field and want its distribution before anything else. It is also the honest pre-check for "is this roughly normal?" before methods that assume normality.

##### What data you need

A single numeric field. The Bins control sets the number of intervals — too few hide structure, too many reveal noise.

##### How to read it

Read four things: centre (where the peak is), spread (width of the mass), skew (a long tail to one side), and modality (one peak, two peaks = two sub-populations). Isolated bars at the edges are outliers.

##### Interpretation & applications

Classify the distribution as symmetric, skewed, or multimodal and decide accordingly. In planning: building heights (character of a skyline), income (inequality), plot sizes, or commute distances. A log transform is often suggested for a strong right skew — the Explore dashboard flags this automatically.

##### Method — equal-width binning

bin width \(w = \dfrac{x_{\max} - x_{\min}}{k}\), bin index \(i = \min\!\left(\lfloor \tfrac{v - x_{\min}}{w} \rfloor,\ k-1\right)\)

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — not animatable; no reference overlays.

See also: Density (KDE) for a smoothed estimate · Box plot for the quantile summary · Violin for shape plus quantiles.
🥧

#### Pie / Donut

chart key: pie · animatable
A part-to-whole display using angle and area. In Cleveland & McGill's hierarchy, angle is a lower perceptual tier than position, so pies are recommended only for few categories where the question is "what share of the whole" rather than "which is larger by how much".

Türkiye employment by sector (2024) — angle encodes each sector's share.

Real data. Services 57.9% · Industry 20.7% · Agriculture 14.8% · Construction 6.6%. Source: TÜİK Labour Force Statistics, 2024.

##### When to use

A handful of categories summing to a meaningful whole (≤ 5–6). The donut variant opens the centre for a total label. The Top-N control collapses a long tail into "Other" so the pie stays readable.

##### What data you need

A categorical field and a numeric value (or a plain count). Each slice is one category's share of the total.

##### How to read it

Compare angles and areas — the largest slice is the dominant category. Don't ask a pie to answer "which is bigger by how much" for many similar slices; angles are hard to compare precisely.

##### Interpretation & applications

Show dominance and balance of a whole. In planning: land-use composition, budget allocation, or housing tenure mix. If you need exact magnitude comparison, switch to a bar chart.

##### Field bindings

Binding | Role
--- | ---
Category | Categorical field
Value | Numeric — aggregated (count / sum / mean / …)

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — animatable (composition over a play axis).

See also: Bar when magnitude comparison matters · Treemap / Sunburst for nested part-to-whole.
📦

#### Box Plot

chart key: box
A compact five-number summary — minimum, Q1, median, Q3, maximum — drawn as a box and whiskers. It compares centre and spread across groups and flags outliers without a single distributional assumption.

İzmir district population by zone — box = IQR, line = median, whiskers = range.

Real data. Central (11 districts) vs North (6) vs South & West (13); the Central zone has the highest and most variable district populations. Source: TÜİK ADNKS 2023.

##### When to use

To compare the distribution — centre, spread, and outliers — of a numeric field across groups. A box plot is the compact alternative to side-by-side histograms when you have several groups.

##### What data you need

A numeric Y field, and optionally a category/group field to produce one box per category. Without a group, one box summarises the whole field.

##### How to read it

The box spans Q1–Q3 (the middle half of the data); the line is the median; the whiskers reach the range within the Tukey fences; dots are outliers. A taller box = more spread; a higher line = higher centre.

##### Interpretation & applications

Compare which groups have higher or lower centres, more or less variability, and where the outliers sit. In planning: house prices, rents, or income by district — the outliers often flag gentrified pockets or distressed areas worth a closer look.

##### Method — quantiles and Tukey fences

quantile \(= x_{[i]}(1-d) + x_{[i+1]}d\), outliers outside \([Q_1 - 1.5\,\mathrm{IQR},\ Q_3 + 1.5\,\mathrm{IQR}]\)

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — not animatable.

See also: Violin for shape · Histogram for the full distribution · Normalised box plot in the dashboard.
🔥

#### Heatmap (Matrix)

chart key: heatmap
A colour-encoded matrix crossing two categorical fields: each cell's colour intensity encodes an aggregated numeric value. It compresses a two-way table into a single image, making dense patterns (correlation structure, origin–destination flows, category co-occurrence) immediately legible.

Türkiye age structure by year (%) — colour intensity encodes each share.

Real data. Nine years × three age groups; the 65+ row brightens from 8.25% (2015) to 10.64% (2024). Source: TÜİK ADNKS.

##### When to use

When two categorical dimensions cross and you want to see the pattern in the resulting matrix — flows, co-occurrence, or a two-way table too large to read as numbers.

##### What data you need

Two categorical fields (X and Y) and a numeric Value field (or a plain count) aggregated per cell.

##### How to read it

Read colour intensity as magnitude. Scan for hot rows or columns (one category that dominates), a hot diagonal (self-interaction), and asymmetry (cell (i,j) differing from (j,i), a one-way flow).

##### Interpretation & applications

Find strong pairings and dominant directions. In planning: origin–destination commute or trip matrices, land-use transitions, or category co-occurrence. A hot diagonal means most flow stays within each zone; off-diagonal heat reveals the dominant corridor.

##### Field bindings

Binding | Role
--- | ---
X | Categorical (rows)
Y | Categorical (columns)
Value | Numeric — cell value, aggregated

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓ — not animatable.

See also: Correlation matrix for the statistical heatmap · Treemap for hierarchical magnitude.
🗂️

#### Treemap

chart key: treemap
A nested-rectangle display of a two-level hierarchy, where each rectangle's area is proportional to a value. It scales to far more categories than a pie chart while preserving part-to-whole reading, built by `transform.tree_rows()`.

İzmir population by zone → district — rectangle area encodes population.

Real data. All 30 districts grouped into Central / North / South & West; the Central zone dominates the area. Source: TÜİK ADNKS 2023.

##### When to use

When a two-level hierarchy has many categories and you want part-to-whole at both levels — outer rectangles are the top-level groups, inner rectangles their sub-division.

##### What data you need

Two grouping fields (outer + inner hierarchy) and a numeric Value field aggregated per group.

##### How to read it

Read area, not linear dimensions — the largest rectangle is the largest value. Outer rectangles group sub-values; compare areas within a group for the sub-composition and across groups for the top-level composition.

##### Interpretation & applications

Spot the dominant sector and its internal composition. In planning: employment by sector → subsector, budget by department → program, or land area by district → land-use class.

##### Field bindings

Binding | Role
--- | ---
Group | Outer + inner hierarchy (two fields)
Value | Numeric — rectangle area, aggregated

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✗ · Matplotlib ✗.

See also: Sunburst for the radial equivalent · Pie for flat part-to-whole.
☀️

#### Sunburst

chart key: sunburst
The radial counterpart of a treemap: concentric rings encode hierarchy levels, with each segment's angular extent proportional to a value. It reads the same two-field structure as the treemap via `transform.tree_rows()` and shines for presentation of a shallow hierarchy.

İzmir population by zone → district — rings are hierarchy levels, angle is population.

Real data. The same 30-district hierarchy, radially — Central dominates, then a long tail of smaller districts. Source: TÜİK ADNKS 2023.

##### When to use

The same two-level hierarchy as a treemap, chosen for its visual impact in presentations and reports. Best for shallow hierarchies where rings stay readable.

##### What data you need

Two grouping fields (outer + inner) and a numeric Value field.

##### How to read it

Read from the centre outward: the inner ring is the top level, each outer ring the next level; a segment's angle is its share. Larger arcs = larger values.

##### Interpretation & applications

Communicate the structure of a whole at two levels. In planning: municipal budget by department → program, or energy consumption by sector → source.

##### Field bindings

Binding | Role
--- | ---
Group | Outer + inner hierarchy (two fields)
Value | Numeric — segment angle, aggregated

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✗ · Matplotlib ✗.

See also: Treemap · Pie.
🎗️

#### Mean ± σ Band

chart key: errorband
A per-category mean drawn with a shaded band one standard deviation either side. It communicates both central tendency and dispersion simultaneously — the honest alternative to a bare average that hides spread.

Türkiye barley yield by period — the bar is the mean, the shaded band is ±1σ.

Real data. 1990–1997 mean 2,179 kg/ha · 1998–2001 mean 2,191 · 2002–2007 mean 2,402 (with a wider band — the 2007 drought). Source: TÜİK crop-production statistics.

##### When to use

When reporting a per-category average and its variability, so the reader sees both the typical value and how consistent it is. The band is ideal for a modest number of categories.

##### What data you need

A categorical X field and a numeric Y field; the mean and sample standard deviation are computed per category.

##### How to read it

The bar (or point) is the mean; the band spans one standard deviation. A wide band = high variability; overlapping bands suggest categories may not differ much, while non-overlapping ones suggest a real difference.

##### Interpretation & applications

Compare both level and reliability across categories. In planning: crop yield by type, air-quality readings by station, or service response times by district — a high mean with a huge band is a very different story from a high mean with a tight band.

##### Method

mean \(\bar{x} = \dfrac{1}{n}\sum x_i\), \(s = \sqrt{\dfrac{1}{n-1}\sum (x_i - \bar{x})^2}\), band \(= [\bar{x} - s,\ \bar{x} + s]\)

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✗.

See also: Mean ± σ bars for the whisker variant · Box plot for a full quantile summary.
📏

#### Mean ± σ Bars

chart key: errorbar
The whisker form of the mean-and-dispersion display: a point for each category's mean with an error bar spanning ±1σ. Cleaner than the band when many categories are compared, and the standard idiom for reporting group means with variability.

İzmir district population by zone — point is the mean, whisker is ±1σ.

Real data. Central mean ≈ 268,000 · North ≈ 89,000 · South & West ≈ 77,000. Source: TÜİK ADNKS 2023.

##### When to use

The same mean-plus-variability story as the band, but for many categories where whiskers read more cleanly than filled bands.

##### What data you need

A categorical X field and a numeric Y field; mean and sample standard deviation are computed per category.

##### How to read it

Compare the points (means) and the whiskers (spread). A longer whisker = more variable; a higher point = higher average. Overlapping whiskers suggest little difference; well-separated ones suggest a real gap.

##### Interpretation & applications

Report group averages with honest uncertainty. In planning: monitoring-station readings, survey scores by neighbourhood, or infrastructure condition by zone.

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✗.

See also: Mean ± σ band · Box plot.
🌊

#### Density (KDE)

chart key: density
Kernel density estimation smooths a histogram into a continuous probability-density curve, revealing modality and shape without the arbitrary choice of bin edges. The Gaussian kernel is used throughout.

İzmir district populations (KDE) — a smoothed curve of the 30-district distribution.

Real data. Most districts cluster below ~150,000 with a long tail toward Buca's 523,487. Source: TÜİK ADNKS 2023.

##### When to use

When you want the smooth shape of a numeric distribution rather than blocky bins — especially to detect multimodality (two or more peaks = sub-populations) that a histogram's bin choice can obscure.

##### What data you need

A single numeric field; an optional Group field overlays one curve per category.

##### How to read it

Read peaks as modes, the width of the curve as spread, and a long tail as skew. Two distinct peaks mean two underlying groups; a single tall peak means one dominant mode.

##### Interpretation & applications

Diagnose the distribution before modelling. In planning: income (bimodality reveals inequality or a divided market), travel times, or property values. With a Group, compare the shape of several districts at once.

##### Method — Silverman bandwidth + Gaussian KDE

\(\hat{h} = 0.9 \min(\hat{\sigma},\ \mathrm{IQR}/1.34)\ n^{-1/5}\)
\(\hat{f}(x) = \dfrac{1}{n\hat{h}\sqrt{2\pi}}\displaystyle\sum_{i=1}^{n}\exp\!\left(-\tfrac{1}{2}\left(\tfrac{x-x_i}{\hat{h}}\right)^{2}\right)\)

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓.

See also: Histogram · Violin (KDE mirrored around a median).
🎻

#### Violin Plot

chart key: violin
A KDE curve mirrored about a central axis, one "violin" per group, with the median marked. It fuses the box plot's quantile summary with the density curve's shape, exposing multimodality, skewness, and tail behaviour that a box hides.

İzmir district population by zone — width is density, the white dot is the median.

Real data. Central (11) vs North (6) vs South & West (13); the Central violin is tallest and widest. Source: TÜİK ADNKS 2023.

##### When to use

When a box plot's five numbers are not enough and you want the shape of each group's distribution — to catch bimodality or skewness a box would flatten away.

##### What data you need

A numeric Y field and an optional category/group field to produce one violin per group.

##### How to read it

Read width as frequency (wider = more data at that value), the white dot as the median, and the thin line as the inter-quartile range. A bulge near the top = many high values; two bulges = bimodal.

##### Interpretation & applications

Compare both shape and level across groups. In planning: test scores by school district (bimodality can flag a divided intake), commute times by zone, or environmental readings by site.

##### Method — mirrored density polygon

`transform.violin_rows()` computes a per-group density, then builds a closed polygon — the left half at `[midpoint − half_width·d/peak, y]`, the right half mirrored. In ECharts this is drawn with a custom `renderItem`; in Vega-Lite a ranged-area workaround avoids the `order` channel so facets never collapse to zero width.

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✓.

See also: Box plot · Density (KDE) · Histogram.
🕸️

#### Radar / Spider

chart key: radar
A multivariate profile drawn on N axes radiating from a centre, one spoke per numeric field. It compares several entities across many variables at once, revealing shape and trade-offs — invaluable for scoring, suitability, and indicator dashboards.

İzmir district profiles — each spoke is an indicator, each polygon a district.

Real data. Konak 327.3k / 125.2 km² / 2,614 ppl/km² / ₺2.9M · Karşıyaka 341.9k / 72.3 km² / 4,728 / ₺3.9M · Bornova 447.6k / 218.6 km² / 2,047 / ₺4.2M (per 100 m²). Source: TÜİK ADNKS 2023; İzmir-MOD; Endeksa 2024.

##### When to use

To compare a few entities across several indicators at once — when the shape of a profile matters more than any single value. Ideal for scoring or suitability profiles where "balanced" versus "specialised" is the question.

##### What data you need

A category X field with at least three values (the axes) and a numeric Y field, plus an optional Group to draw one polygon per entity.

##### How to read it

Each spoke is one indicator (its own scale); a point far from the centre is high on that indicator. Compare polygon shapes — a large, balanced polygon is strong overall; a lopsided one is strong on some axes, weak on others.

##### Interpretation & applications

Expose strengths, weaknesses, and trade-offs per entity. In planning: district quality-of-life, site suitability, or service-coverage scoring — where the visual "footprint" of each alternative is the message.

##### Method — per-axis maxima

Axes are computed by `transform.radar_axis_maxes()`, padding each field's maximum by 5% so the top of a profile never touches the outer ring. Fields are normalised against their own maxima, so differently-scaled indicators compare fairly.

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✗ · Matplotlib ✗.

See also: Error bars for per-variable spread · Normalisation for the same scaling idea on map diagrams.
📉

#### Pareto (80/20)

chart key: pareto
A bar chart of categories sorted descending, overlaid with a cumulative-share line. It operationalises Juran's (1954) principle — the vital few versus the trivial many — letting an analyst see instantly where the bulk of a total concentrates.

İzmir district population (80/20) — bars sorted descending, line is the cumulative share.

Real data. The eight largest districts hold 2,846,877 of İzmir's 4,479,525 people (63.6%). Source: TÜİK ADNKS 2023.

##### When to use

When you need to rank causes, defects, or demands by impact and identify the few that account for most of the total — the classic prioritisation chart.

##### What data you need

A categorical X field and a numeric value (count or sum). Categories are sorted descending automatically.

##### How to read it

Bars descend from largest to smallest; the line climbs to 100%. The leftmost bars before the line reaches ~80% are the "vital few" that dominate the total; the long tail after is the "trivial many".

##### Interpretation & applications

Prioritise interventions on the few categories that matter most. In planning: the top complaint types, the leading causes of permit delays, or the dominant land-use conflicts — focus effort where the line crosses 80%.

##### Method — cumulative share

cumulative share \(s_i = \dfrac{\sum_{j \le i} v_j}{\sum v} \times 100\)

##### Engine support

ECharts ✓ · Plotly ✓ · Vega-Lite ✓ · Matplotlib ✗.

See also: Bar · Insights (which reports the top-category share automatically).
GROUP 3

### Map Diagrams

🧁

#### Diagram Types

core/diagrams.py · native QgsDiagramRenderer
Map diagrams draw a small chart on every feature, using QGIS's native diagram renderer so the result is a real symbology that prints and exports like any other. Four types are available, all sized in millimetres and coloured with the active studio palette.

##### Types

Type | QGIS class | Encodes
--- | --- | ---
Pie | QgsPieDiagram | Composition of several numeric fields per feature
Bar | QgsHistogramDiagram | Side-by-side values of several fields
Stacked bar | QgsStackedBarDiagram (QGIS 3.14+) | Additive composition of fields
Text | QgsTextDiagram | Raw values as compact text

Diagram size spans 3–60 mm (default 14 mm). Placement adapts to geometry type (AroundPoint / Line / OverPoint).

See also: Normalisation · Size-by · Palettes (diagram colours follow the studio palette).
⚖️

#### Diagram Normalisation

core/expressions.py · baked into the diagram expression
A pie or bar comparing fields on very different scales is dominated by the largest-magnitude field. Normalisation rescales each field before it is drawn, so every component contributes fairly. Statistics are computed from the features and baked into the diagram expressions — nothing is written back to the data.

##### Modes

Mode | Expression | Use case
--- | --- | ---
None | \(v\) | Same-scale fields (e.g. population components)
Min–max (0–1) | \(\dfrac{v - \min}{\max - \min}\) | Different ranges on one diagram
Z-score | \(\dfrac{v - \mu}{\sigma}\) | Statistical comparison; warns for pies (negative angles impossible)
Log | \(\ln(v - \min + 1)\) | Heavy-tailed distributions

Watch the hint. The dock's `_sync_diag_hint()` warns for mathematically problematic choices — Z-score on a pie chart produces negative angles, and Log on values with zero or negative minima is undefined.
See also: Diagram types · Statistics.
🔍

#### Size-by (Proportional Symbols)

Size by · Avoid overlaps · Size (mm)
The Size by control scales each diagram between a small and a full-size one according to a chosen field, turning every feature into a proportional symbol. It is the classic thematic-map idiom — larger value, larger diagram — layered on top of any of the four diagram types.

##### Controls

Control | Effect
--- | ---
Size by | Field | Scales diagram size between min and full size
Size (mm) | 3–60 mm | Full-size diagram; the small end is scaled proportionally
Avoid overlaps | Checkbox | Let QGIS displace diagrams to reduce collision

See also: Diagram types · Normalisation.
GROUP 4

### Labels

🏷️

#### Label Presets

core/labels.py · QgsPalLayerSettings
Four curated label styles, each a named combination of halo, weight, and treatment, applied via native `QgsPalLayerSettings` and `QgsVectorLayerSimpleLabeling` — so labels behave, render, and export like any QGIS labeling.

##### Presets

Preset | Treatment | Use
--- | --- | ---
Clean | 0.6 mm white halo | Subtle, cartographic default
Strong halo | 1.2 mm halo | Labels survive busy basemaps
Bold | Heavy weight | Emphasis, no halo
Plain | No halo | Minimal, dense layouts

See also: Formatting.
✍️

#### Label Formatting

core/expressions.py · single concat() expression
Format a label without leaving the dock. Fields and formatting are combined into a single QGIS `concat()` expression, with a live preview showing the assembled expression and the first feature's evaluated value.

##### Formatting capabilities

Control | Effect
--- | ---
Primary / Second field | Two fields; second row joined with `char(10)`
Decimals | −1 to 6 via `round()`
Thousands separator | `format_number()`
Prefix / Suffix | Wrapped with `concat()`
Case | upper / lower / title
Word wrap | `wordwrap()` with configurable limit
Advanced expression | Freeform QGIS expression overrides all formatting; validated

Number safety. Numeric fields receive rounding and number formatting; text fields never do — 02viz never applies a numeric function to a string and corrupts the label.
See also: Presets · Smart Assistant (which proposes a two-line label when name + measurement fields both exist).
GROUP 5

### Studio Features

🖥️

#### Explore Dashboard

core/profile.py · engines/dashboard.py · one click
The Explore layer button profiles every field of a layer and renders a complete interactive dashboard in one operation — an automated exploratory data analysis (Tukey, 1977) with toggleable tiles.

The one-click Explore dashboard: KPI cards, field summaries, auto charts, correlation matrix, and a trend scatter.

##### Tiles

Tile | Contents
--- | ---
KPI cards | Row count, field counts (total / numeric / categorical), cell completeness %
Field summary | Per-field type badge, missing % (green <5%, amber 5–20%, red >20%), distinct count, range / top category
Categorical bars | Up to 4 fields, Top-N control
Histograms | Up to 6 numeric fields, 14 bins each
Normalised box plot | All numeric fields on one 0–1 axis (min–max normalised) for cross-scale comparison
Correlation matrix | Pairwise Pearson \(r\), diverging colour (blue-negative / red-positive), labelled cells, up to 8 fields
Scatter + trend | Strongest \(|r|\) field pair, sampled to 3,000 points, with trend line
Insights | Plain-English chips — dominant category, range, skew with log hint, outliers, near-constant and mostly-empty fields, notable correlations

Identifier columns (`fid`/`id`/`gid`/`uuid`) are skipped automatically.

See also: Statistics · Smart Assistant.
🎞️

#### Animation (Play Axis)

core/transform.py · build_frames() · 6 chart types
Pick a time or sequence field and any of six chart types plays through it — bar-chart race, Gapminder bubbles, or composition unfolding over the years — with axes and colours held steady so values animate in place. This axis stability follows Tufte's (1983) principle of holding the frame of reference constant.

##### The five-stage pipeline

1. `frame_groups()` partitions rows by the animate field, ordered numerically or lexicographically.
2. `union_categories()` merges all frames' categories into a stable axis order.
3. `align_values()` pads missing categories with zeros.
4. Global numeric ranges are fixed across frames so axes never rescale.
5. Bubble radii are globally scaled once so the same value maps to the same size across frames.

##### Playback & scope

Control | Values
--- | ---
Speed | Slow (1600 ms/frame) · Medium (900) · Fast (450)
ECharts | timeline with autoPlay + loop
Plotly | frames + slider + play/pause
Eligible | bar · line · area · scatter · bubble · pie

Reference overlays are hidden during animation. Animated bars and points still click to select features on the map, and the animation exports in the one-file HTML.

See also: Bubble · Overlays.
➖

#### Reference Overlays

core/overlays.py · 5 overlays · 5 chart types
Drop statistical reference lines and bands onto bar, line, area, scatter, and bubble charts: the mean, the median, a target value you type, a ±1σ band, and the inter-quartile (Q1–Q3) band. Computed in pure Python from the plotted values and drawn identically by all four engines, so they read the same in light and dark and export with the chart.

##### Overlays

Overlay | Computation | Style
--- | --- | ---
Mean | \(\bar{x} = \frac{1}{n}\sum x_i\) | Dashed line + label
Median | Linear-interpolation median | Dotted line + label
±1σ band | \([\bar{x} - s,\ \bar{x} + s]\) | Shaded region
IQR band | Shaded Q1 → Q3 | Shaded region + label
Target | User-entered constant | Solid emphasis line

##### Engine rendering

ECharts `markLine`/`markArea` · Plotly `layout.shapes` · Vega-Lite rule/rect layers · matplotlib `axhline`/`axhspan`. Hidden during animation.

See also: Statistics · Bar.
💡

#### Smart Assistant

core/assistant.py · 265 lines · zero network
The Suggest a chart button reads the active layer's fields and configures the most insightful chart for you — telling you why. It is a pure-Python, zero-network recommendation engine: no account, no internet, no new dependencies.

##### Recommendation heuristics

Field signature | Recommendation
--- | ---
Two numeric, \(|r| \ge 0.4\) | Scatter + trend line
Categorical + numeric | Mean bar
Categorical only | Count bar
Numeric only | Histogram

`suggestions()` also evaluates the other tabs: diagram normalisation (min–max / log when a field-range ratio ≥ 50×) and label configuration (two-line when name + measurement fields are both present).

See also: Explore · Field classification.
💾

#### Chart Presets

core/presets.py · schema v1 · up to 50
Save a named chart setup and reuse it across QGIS sessions — and across layers. A preset records engine, chart type, field bindings, aggregation, bins, Top-N, sort, stacked/trend flags, overlays, theme, palette, and animation speed. On load, fields remap by name; unavailable fields are cleared and reported.

##### What a preset stores

Engine · chart type · field bindings (by name) · aggregation · bins · Top-N · sort · stacked / trend flags · reference overlays · theme · palette · animation speed. Stored as JSON (schema v1) in `QSettings`.

See also: Themes & palettes.
🔗

#### Chart ↔ Map Selection

title transport · SelectionBridge · cross-filter
Charts and the map stay linked in both directions. Click a bar, slice, or point and the matching features are selected on the canvas; select features on the canvas and the chart instantly dims everything else — no re-render.

##### How the bridge works

Chart pages encode clicked feature IDs as `document.title = "o2viz-select:<id,id,…>:<seq>"` via `__o2vizSelect()`; the dock's `titleChanged` listener forwards to `SelectionBridge`, which calls `layer.selectByIds(ids)`. Reverse cross-filter pushes `__o2vizHighlight(ids)` to dim non-selected items to opacity 0.16. Selection lists over 20,000 IDs skip highlighting for performance.

Why title transport. `addToJavaScriptWindowObject` crashes inside WebCore on Qt 5.15's QtWebKit fork, and `QWebChannel` exists only on WebEngine. The page title is the one signal safe and identical on both stacks — in a plain browser it is a harmless blip.
See also: Architecture · Animation (animated points still select).
🎨

#### Themes & Palettes

engines/base.py · 4 themes · 8 palettes · swatch editor
Four themes and eight palettes give every chart a consistent typographic and colour voice; an inline swatch editor recolours any palette, and the override propagates identically across ECharts, Plotly, and Vega-Lite.

##### Themes

Theme | Background | Use
--- | --- | ---
Studio Light | #fbfbfd | Default
Ink Dark | #131c21 | Presentations
Soft Pastel | #ffffff | Muted, light content
Bold Print | #ffffff | Publication-ready monochrome

##### Palettes

Vivid (high-chroma) · Colorblind safe (8-colour, Wong 2011 guidance) · Viridis (perceptually uniform sequential) · Sunset (warm orange→purple) · Ocean (cool teal→navy) · Earth (beige→forest) · Berry (red-violet) · Grayscale print (distinct lightness steps).

##### Custom swatches

Inline swatches right in the dock — click one to recolour it via `QColorDialog`, `+`/`−` to resize the palette (up to 16 colours). Auto-switches to "Custom…" mode.

See also: Presets · Map diagrams (which follow the active palette).
🧮

#### Data Shaping & Statistics

core/stats.py · core/transform.py · pure Python
Every statistic and data-shaping operation is pure Python — no pandas, no numpy. The result is a studio that runs identically offline, in tests, and inside QGIS.

##### Methods

- Aggregation — count, sum, mean, median (linear interpolation), min, max.
- Pearson correlation — \(r = \frac{\sum(x_i-\bar{x})(y_i-\bar{y})}{\sqrt{\sum(x_i-\bar{x})^2}\sqrt{\sum(y_i-\bar{y})^2}}\), with zero-variance degeneracy checks.
- Skewness — adjusted Fisher–Pearson \(g_1 = \frac{\sqrt{n(n-1)}}{n-2}\cdot\frac{m_3}{m_2^{3/2}}\); a log-transform hint fires when \(|g_1| \ge 0.5\).
- Outliers — Tukey inner fences \([Q_1 - 1.5\,\mathrm{IQR},\ Q_3 + 1.5\,\mathrm{IQR}]\).
- KDE — Silverman bandwidth, Gaussian kernel.
- Box plots — linear-interpolation quantiles.
- Field classification — numeric (≥60% numeric, >8 distinct), categorical (≤30 distinct), identifier (skip).
- Data shaping — group-by, pivot, Top-N with "Other" collapse, per-frame slicing, bubble sqrt-scaling, trend line, treemap/sunburst hierarchies.

See also: KDE · Box plot · Dashboard.

## Methodological Notes

Design decisions worth recording — the "why" behind the plugin's behaviour, drawn from the visualisation and cartography literature.

Position over angle. Cleveland & McGill (1984) rank position along a common scale as the most accurately perceived encoding and angle as one of the least. 02viz defaults to bar charts for magnitude comparison and reserves pie/angle encoding for few-category part-to-whole questions.
Area, not radius. The eye perceives bubble area, so radius is scaled by square root (\(\sqrt{v}\)) to keep perceived size proportional to value. The same principle governs map-diagram "Size by", which scales diagram area rather than linear extent.
Hold the frame of reference. Following Tufte (1983), animation fixes axes, category order, colours, and bubble scaling across frames so movement reads as data change rather than axis rescaling. Reference overlays are hidden during animation for the same reason.
Normalisation is not data edit. Diagram normalisation (min–max / Z-score / log) is baked into the diagram expression from feature statistics; it never writes new columns, so the source layer is untouched. Z-score on a pie chart produces negative angles — the dock warns rather than silently rendering nonsense.
Pure Python by construction. Aggregation, correlation, skewness, KDE, quantiles, and outlier detection are implemented directly rather than delegated to pandas/numpy, keeping the core studio dependency-free and its behaviour verifiable in plain unit tests.

## Academic References

1. Wilkinson, L. (2005). The Grammar of Graphics (2nd ed.). Springer. DOI: 10.1007/0-387-28695-0
2. Tufte, E. R. (1983). The Visual Display of Quantitative Information. Graphics Press.
3. Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley.
4. Cleveland, W. S. & McGill, R. (1984). Graphical perception: Theory, experimentation, and application to the development of graphical methods. Journal of the American Statistical Association, 79(387), 531–554. DOI: 10.1080/01621459.1984.10478080
5. Satyanarayan, A., Moritz, D., Wongsuphasawat, K., & Heer, J. (2017). Vega-Lite: A grammar of interactive graphics. IEEE Transactions on Visualization and Computer Graphics, 23(1), 341–350. DOI: 10.1109/TVCG.2016.2599030
6. Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90–95. DOI: 10.1109/MCSE.2007.55
7. Li, D., Mei, H., Shen, Y., Su, S., Zhang, W., Wang, J., Zu, M., & Chen, W. (2018). ECharts: A declarative framework for rapid construction of web-based visualization. Visual Informatics, 2(2), 136–146. DOI: 10.1016/j.visinf.2018.04.011
8. Silverman, B. W. (1986). Density Estimation for Statistics and Data Analysis. Chapman & Hall. DOI: 10.1007/978-1-4899-3324-9
9. Wong, B. (2011). Points of view: Color blindness. Nature Methods, 8(6), 441. DOI: 10.1038/nmeth.1618
10. Juran, J. M. (1954). Universals in management planning and controlling. Management Review, 43(11), 748–761.