02viz

Geospatial Visualization Studio — multi-engine chart, diagram, and label studio • v0.14.2

1. Overview

02viz transforms the QGIS environment into a comprehensive data visualisation studio. It provides a single dock with three tabs—Charts, Map Diagrams, and Labels—all sharing a common layer and data selector. Four rendering engines (two zero-dependency offline JavaScript engines plus an optional Python engine) produce interactive, publishable charts from QGIS vector layers or external tabular data (CSV, XLSX, ODS, GPKG, DBF). The plugin runs entirely offline with no accounts, no network, and no mandatory external Python dependencies.

The plugin ships with bundled, vendored copies of Apache ECharts, Plotly.js, Vega, and Vega-Lite in web/. The chart-to-map selection bridge uses the browser's document.title as a click-event transport channel—a design driven by the observation that addToJavaScriptWindowObject causes access violations in WebCore during page commit on Qt 5.15's QtWebKit fork. QWebChannel exists only on QtWebEngine, making titleChanged the single cross-platform signal for chart-to-map interaction.

2. Theoretical Foundations

2.1 The Grammar of Graphics

The plugin's architecture follows Wilkinson's (2005) Grammar of Graphics, which decomposes statistical graphics into independent components: data, aesthetic mappings, geometric objects, scales, statistical transformations, coordinate systems, and facets. The spec assembly layer (core/transform.py and dock.py) produces a data dictionary with typed rows and metadata, which each engine reinterprets through its own rendering primitives. This separation of data shaping from rendering is the central architectural principle—every engine exposes only build_html(spec) → str, consuming the same data structure and producing a fully self-contained HTML document.

Vega-Lite (Satyanarayan et al., 2017), one of the four engines, is a direct realisation of the Grammar of Graphics as a declarative JSON specification language. Its compiler maps visual encoding channels to lower-level Vega rendering directives, providing theoretical continuity from Wilkinson's abstract framework to an executable visualisation. The plugin exposes a custom Vega-Lite spec editor, allowing users to directly manipulate the declarative specification.

2.2 Exploratory Data Analysis

The Smart Assistant and Explore Dashboard operationalise Tukey's (1977) framework for exploratory data analysis (EDA). Fields are classified into numeric, categorical, and identifier categories; visualisations are selected based on the data type combination. This follows Tukey's principle that visual summaries should precede formal modelling. The Explore Dashboard produces KPI cards, histograms, box plots, correlation matrices, and scatter plots in a single operation, enabling rapid assessment of distribution shape, central tendency, spread, association, and outliers.

2.3 Perceptual Principles

The plugin follows Cleveland and McGill's (1984) ranking of graphical perception accuracy. Position along a common scale (bar charts, line charts) is used for the most critical comparisons. Angle (pie charts) occupies a lower perceptual tier and is recommended only for part-to-whole comparisons with few categories. Area (bubble charts) uses square-root scaling via transform.bubble_sizes(): radii follow \(r \propto \sqrt{v}\) so that perceived area—not radius—is proportional to the data value, correcting the systematic underestimation that occurs with linear radius scaling.

3. Rendering Engines

EngineChart TypesAnimationWebKit OKInteractiveLibrary Size
Apache EChartsAll 17Timeline (autoPlay, loop)YesFull cross-filter~1 MB (Apache-2.0)
Plotly.jsAll 17Frames + slider + play/pauseNo (ES6+)Full cross-filter~4.5 MB (MIT)
Vega-Lite14NoneNoDeclarative; JSON editor~0.76 MB (BSD-3)
Matplotlib / seaborn11NoneYes (PNG)None (static)Optional pip

3.1 Apache ECharts Engine

ECharts (Li et al., 2018) is a declarative framework optimised for interactive web exploration. The plugin's ECharts engine (engines/echarts.py, 568 lines) produces self-contained HTML with the library inlined. It uses the timeline component for animation, markLine and markArea for reference overlays, and custom renderItem functions for violin plots (polygon rendering) and error-bar whiskers. As the only engine with full ES5 compatibility, it is the default on QGIS builds using Qt 5.15's QtWebKit fork.

3.2 Plotly.js Engine

The Plotly engine (engines/plotly.py, 466 lines) uses Plotly.js's frames-and-slider animation with play/pause updatemenus. Overlays are rendered as layout.shapes (lines and rectangles) with layout.annotations. Cross-filter highlighting uses Plotly.restyle() with per-trace selected.marker.opacity and unselected.marker.opacity attributes. On WebKit-only QGIS builds, a styled "explainer" fallback page informs the user why the panel is blank and offers the options of switching to ECharts, exporting to HTML, or opening in the system browser.

3.3 Vega-Lite Engine

The Vega-Lite engine (engines/vegalite.py, 573 lines) compiles declarative JSON specifications to Vega at runtime via vegaLite.compile(). It supports 14 of 17 chart types; treemap, sunburst, and radar are excluded because these hierarchical and radial layouts are not part of the Vega-Lite grammar. Users can edit the generated JSON specification directly with validation before rendering. Data rows carrying feature IDs are injected as a named dataset o2viz. The violin chart workaround avoids the order channel, using a ranged-area mark with re-paired polygon half-points to prevent the facets from collapsing to zero width.

3.4 Matplotlib / Seaborn Engine

The Matplotlib engine (engines/mpl.py, 276 lines) is the only engine with optional Python dependencies. It imports matplotlib and seaborn lazily, uses matplotlib.use("Agg") for headless rendering, and applies seaborn's set_theme(style="whitegrid") when available. Output is an 8.2" x 5.0" figure at 150 DPI, base64-encoded as PNG. Eleven chart types are supported. Optional dependency status is detected at startup via core/requirements.py without any pip invocation.

4. Chart Types (17)

ChartEChartsPlotlyVega-LiteMatplotlibData Requirement
BarYesYesYesYesX: categorical or numeric; Y: numeric
LineYesYesYesYesX: ordered; Y: numeric
AreaYesYesYesYesX: ordered; Y: numeric
ScatterYesYesYesYesX, Y: numeric
BubbleYesYesYesYesX, Y, Size: numeric
HistogramYesYesYesYesX: numeric; Y: frequency (auto)
Pie / DonutYesYesYesYesCategory: categorical; Value: numeric
Box PlotYesYesYesYesX: categorical (opt.); Y: numeric
HeatmapYesYesYesYesX, Y: categorical; Value: numeric
TreemapYesYesTwo-level hierarchy; Value: numeric
SunburstYesYesTwo-level hierarchy; Value: numeric
Mean ± σ bandYesYesYesX: categorical; Y: numeric grouped
Mean ± σ barsYesYesYesX: categorical; Y: numeric grouped
Density (KDE)YesYesYesYesX: numeric; group optional
Violin PlotYesYesYesYesX: categorical (opt.); Y: numeric
Radar / SpiderYesYesMultiple numeric fields per category
Pareto (80/20)YesYesYesCategory: categorical; Value: numeric

4.1 Bar and Scatter Charts

Bar charts encode numeric values using position along a common scale, the highest-accuracy channel in Cleveland and McGill's hierarchy. Grouped bars use the Group field for multi-series display; stacked bars use the stacked checkbox for additive composition. The Top-N control collapses categories beyond N into "Other".

Scatter plots display bivariate numeric relationships with optional group-colouring and least-squares trend line: \(\beta = S_{xy} / S_{xx}\), \(\alpha = \bar{y} - \beta\bar{x}\). Bubble charts add a third numeric dimension with square-root-scaled radii: \(r = r_{\min} + (r_{\max} - r_{\min}) \sqrt{(v - v_{\min})/(v_{\max} - v_{\min})}\). During animation, bubble radii are scaled globally so the same value maps to the same size across all frames.

4.2 Histograms and Kernel Density Estimation

Histograms use equal-width binning via stats.histogram(): bin width \(w = (x_{\max} - x_{\min}) / k\), index \(i = \min(\lfloor (v - x_{\min}) / w \rfloor, k-1)\).

Kernel density estimation uses a Gaussian kernel with Silverman's (1986) rule-of-thumb bandwidth:

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

The grid extends one bandwidth past the data range so the density tapers to approximately zero at boundaries. All computation is pure Python—no numpy, scipy, or pandas dependency.

4.3 Box Plots and Violin Plots

Box plots display the five-number summary (min, Q1, median, Q3, max) via linear-interpolation quantiles: for position \(p = q(n-1)\), integer index \(i = \lfloor p \rfloor\), fraction \(d = p - i\), the quantile is \(x_{[i]}(1-d) + x_{[i+1]}d\). Outlier detection uses Tukey fences: values outside \([Q_1 - 1.5 \times \mathrm{IQR}, Q_3 + 1.5 \times \mathrm{IQR}]\).

Violin plots build on KDE: transform.violin_rows() computes per-group density, then constructs a closed polygon—the left half at [midpoint - half_width * d/peak, y], right half mirrored. The resulting shape reveals multimodality, skewness, and tail behaviour invisible in a box plot.

4.4 Pareto and Other Charts

Pareto charts operationalise Juran's (1954) principle: categories are sorted descending by value, cumulative share \(s_i = \sum v_j / \sum v \times 100\) is overlaid as a line. Treemap and sunburst charts use two-level nested hierarchies built by transform.tree_rows(), rendering as ECharts or Plotly hierarchical layouts. Radar charts compute per-axis maxima padded by 5% via transform.radar_axis_maxes().

5. Charts Tab

5.1 Data Specification

The shared data card provides: a QgsMapLayerComboBox filtered to vector layers, an external file loader (CSV, XLSX, ODS, GPKG, DBF), a "selected features only" checkbox (auto-refreshes on selectionChanged), and a "live refresh" checkbox (re-renders on layer edit, capped at 100,000 rows). The last I/O directory persists via QSettings("zero2viz/last_io_dir").

5.2 Field Classification

The field classifier (core/fields.py) assigns each attribute: numeric (≥60% numeric with >8 distinct values), categorical (≤30 distinct values, unless all unique with >6 rows), or skip (identifiers matching fid, id, gid, uuid, objectid, *_id, *_key). The "Suggest a chart" button triggers assistant.suggest_chart() with priority heuristics: two numeric fields with \(|r| \ge 0.4 \rightarrow\) scatter+trend; categorical+numeric \(\rightarrow\) mean bar; categorical only \(\rightarrow\) count bar; numeric only \(\rightarrow\) histogram.

5.3 Themes and Palettes

Four Themes, Eight Palettes, Custom Editor

Themes: Studio Light (default, #fbfbfd), Ink Dark (#131c21, presentations), Soft Pastel (#ffffff, muted), 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 palette editor: Inline swatch editor with click-to-change via QColorDialog and +/- buttons (up to 16 colours). Auto-switches to "Custom..." mode.

5.4 Chart-to-Map Selection Bridge

Title-Based Click Transport

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 (map-to-chart) pushes __o2vizHighlight(ids) to dim non-selected items to opacity 0.16. Selection lists exceeding 20,000 IDs skip highlighting for performance. The title-transport approach was adopted because addToJavaScriptWindowObject crashes in Qt 5.15's WebKit fork, and QWebChannel exists only on WebEngine.

6. Statistical Methods

6.1 Aggregation and Correlation

All aggregation is pure Python in transform.py: Count, Sum, Mean (\(\bar{x} = \frac{1}{n}\sum x_i\)), Median (linear interpolation), Min, Max. The Pearson correlation by stats.pearson() uses the standard formula with zero-variance degeneracy checks:

\(r = \dfrac{\sum (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum (x_i - \bar{x})^2} \sqrt{\sum (y_i - \bar{y})^2}}\)

6.2 Skewness and Outliers

Adjusted Fisher-Pearson sample skewness:

\(g_1 = \dfrac{\sqrt{n(n-1)}}{n-2} \cdot \dfrac{m_3}{m_2^{3/2}}, \quad m_k = \frac{1}{n}\sum (x_i - \bar{x})^k\)

The Explore Dashboard reports skewness with a log-transform hint when \(|g_1| \ge 0.5\). Outlier counts use Tukey's inner fences.

7. Map Diagrams Tab

Native QGIS Per-Feature Diagrams

Four diagram types via QgsDiagramRenderer: Pie (QgsPieDiagram), Bar (QgsHistogramDiagram), Stacked bar (QgsStackedBarDiagram, QGIS 3.14+), and Text (QgsTextDiagram). Size: 3–60 mm (default 14 mm). Placement adapts to geometry type (AroundPoint/Line/OverPoint).

ModeExpressionUse Case
NoneRaw valuesSame-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

Normalisation expressions are baked into the diagram's classified expression using pre-computed field statistics via core/expressions.py—no new columns are written to source data. The dock's _sync_diag_hint() provides contextual warnings for mathematically problematic normalisation choices. Diagram colours use the active studio palette for visual consistency.

8. Labels Tab

Expression-Driven Feature Labels

Uses QgsPalLayerSettings and QgsVectorLayerSimpleLabeling. Expressions are built by core/expressions.py, combining fields and formatting into a single concat() expression.

Four presets: Clean (0.6 mm white halo), Strong halo (1.2 mm), Bold (heavy weight), Plain (no halo).

Formatting: primary field, optional second-row field (joined with char(10)), decimal places (−1–6 via round()), thousands separator (format_number()), prefix/suffix (concat()), case conversion (upper/lower/title), word wrap (wordwrap() with configurable limit).

Number safety: Numeric fields receive rounding/number formatting; text fields never do. Advanced expression: Freeform QGIS expression overrides all formatting; validated before application. Live preview: Shows assembled expression and first feature's evaluated value, updating on every control change.

9. One-Click Explore Dashboard

Full Layer Analysis in One Click

The "Explore layer" button triggers core/profile.py's build_profile(), rendered as a responsive ECharts dashboard via engines/dashboard.py. Tiles (toggleable via Tile Picker):

KPI Cards: Row count, field counts (total/numeric/categorical), cell completeness %.

Field Summary Table: Per-field type badge, missing % (green <5%, amber 5–20%, red >20%), distinct count, context-sensitive summary.

Categorical Bar Charts: Up to 4 fields with Top-N control.

Histograms: Up to 6 numeric fields, 14 bins each.

Normalised Box Plots: All numeric fields on one 0–1 axis via min-max normalisation for cross-scale comparison.

Correlation Matrix: Pairwise Pearson \(r\) for up to 8 fields. Diverging colour (blue-negative, red-positive) with labelled cells.

Strongest-Correlation Scatter: The field pair with the highest \(|r|\), sampled to 3,000 points, with trend line.

Text Insights: Strongest correlations, widest range, skewness with log hints, outlier counts, near-constant fields, high-null fields, top category share.

10. Animation (Play Axis)

Temporal and Sequence Animation

Available for Bar, Line, Area, Scatter, Bubble, and Pie charts via core/transform.py's build_frames():

  1. frame_groups() partitions rows by the animate field value, 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 all frames so axes do not rescale.
  5. Bubble radii are globally scaled once so the same value maps to the same size across frames.

Playback: Slow (1600 ms/frame), Medium (900 ms), Fast (450 ms). ECharts uses timeline with autoPlay/loop; Plotly uses frames with slider/play-pause. Reference overlays are hidden during animation. Axis stability follows Tufte's (1983) principle of holding the frame of reference constant.

11. Reference Overlays

Statistical Reference Lines and Bands

Available for Bar, Line, Area, Scatter, Bubble. Computed in pure Python by core/overlays.py:

OverlayComputationStyle
Mean line\(\bar{x} = \frac{1}{n}\sum x_i\)Dashed line with label
Median lineLinear-interpolation medianDotted line with label
±1σ band\([\bar{x} - s, \bar{x} + s]\)Shaded region
IQR bandShaded from Q1 to Q3Shaded region with label
Target valueUser-entered constantSolid emphasis line

Rendered via: ECharts markLines/markAreas, Plotly layout.shapes, Vega-Lite rule/rect layers, matplotlib axhline/axhspan. Hidden during animation.

12. Smart Chart Assistant

Offline Recommendation Engine

core/assistant.py: pure-Python, zero-network. suggest_chart() analyses field types and recommends the most suitable chart with full control configuration. suggestions() also evaluates diagram normalisation (min-max/log when field-range ratio ≥50x) and label configuration (two-line when name+measurement fields both present).

13. Chart Presets

Named, Versioned, Cross-Layer Presets

Up to 50 presets stored in QSettings as JSON (schema v1). Each stores: engine, chart type, field bindings (by name), aggregation, bins, Top-N, sort, stacked/trend flags, overlays, theme, palette, animation speed. Fields remap by name across layers; unavailable fields are cleared with a status report.

14. Controls Summary

Shared Data Card

ControlDescription
Layer comboQgsMapLayerComboBox (vector) or external file (CSV/XLSX/ODS/GPKG/DBF)
Selected features onlyUses only selected features; auto-refreshes on selectionChanged
Live refreshRe-renders on layer edit; max 100,000 rows; guarded for deleted C++ objects

Charts Tab Controls

ControlDescription
EngineECharts / Plotly / Vega-Lite / Matplotlib
Chart type17 types; unsupported types greyed per engine; field selectors adapt contextually
Field bindingsX, Y, Group, Value, Animate by (context-sensitive per type via _CONTROLS matrix)
AggregationCount / Sum / Mean / Median / Min / Max
Bins / Top-N / SortContext-sensitive; Top-N collapses remainder to "Other"
Stacked / Trend lineCheckboxes; trend = least-squares linear regression
Reference overlaysMean, median, ±1σ band, IQR band, target value
Theme / Palette4 themes; 8 palettes + custom inline swatch editor
Animation speedSlow (1600 ms) / Medium (900 ms) / Fast (450 ms)

Map Diagrams Tab Controls

ControlDescription
Diagram typePie / Bar / Stacked bar / Text
Size (mm)3–60 mm, default 14
FieldsMulti-select numeric field checkboxes
NormalisationNone / Min-max / Z-score / Log; warns for pie+Z-score
Apply / RemoveSets or clears diagram renderer

Labels Tab Controls

ControlDescription
PresetClean / Strong halo / Bold / Plain
Primary / Second fieldQgsFieldComboBox per row
Decimals / Separator−1–6; thousands separator checkbox
Prefix / Suffix / Case / WrapString; dropdown; spinbox
Advanced expressionFreeform QGIS expression (overrides formatting); validated
Live previewExpression text + first feature evaluation

15. Workflow Guide

  1. Click the 02viz toolbar icon to toggle the dock.
  2. Data: Select a vector layer or load external file. Optionally enable "selected features only" or "live refresh".
  3. Charts: Choose engine (ECharts recommended). Select chart type and bind fields. Click Suggest a chart for automatic recommendations. Set aggregation, bins, Top-N. Click Render chart. Click elements to select features. Export as HTML/SVG/PNG/PDF.
  4. Diagrams: Switch to Diagrams tab. Check fields, choose type/size/normalisation. Click Apply to layer.
  5. Labels: Switch to Labels tab. Choose fields, configure formatting, verify in preview. Click Apply to layer.
  6. Explore: Click "Explore layer" for full statistical dashboard. Use Tile Picker to toggle tiles.
  7. Animation: Select an "Animate by" field. Chart renders with timeline controls. Optionally set speed.
  8. Presets: Save/load chart configurations. Fields remap by name across layers.

16. Technical Notes

17. Literature

  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.