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
| Engine | Chart Types | Animation | WebKit OK | Interactive | Library Size |
|---|---|---|---|---|---|
| Apache ECharts | All 17 | Timeline (autoPlay, loop) | Yes | Full cross-filter | ~1 MB (Apache-2.0) |
| Plotly.js | All 17 | Frames + slider + play/pause | No (ES6+) | Full cross-filter | ~4.5 MB (MIT) |
| Vega-Lite | 14 | None | No | Declarative; JSON editor | ~0.76 MB (BSD-3) |
| Matplotlib / seaborn | 11 | None | Yes (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)
| Chart | ECharts | Plotly | Vega-Lite | Matplotlib | Data Requirement |
|---|---|---|---|---|---|
| Bar | Yes | Yes | Yes | Yes | X: categorical or numeric; Y: numeric |
| Line | Yes | Yes | Yes | Yes | X: ordered; Y: numeric |
| Area | Yes | Yes | Yes | Yes | X: ordered; Y: numeric |
| Scatter | Yes | Yes | Yes | Yes | X, Y: numeric |
| Bubble | Yes | Yes | Yes | Yes | X, Y, Size: numeric |
| Histogram | Yes | Yes | Yes | Yes | X: numeric; Y: frequency (auto) |
| Pie / Donut | Yes | Yes | Yes | Yes | Category: categorical; Value: numeric |
| Box Plot | Yes | Yes | Yes | Yes | X: categorical (opt.); Y: numeric |
| Heatmap | Yes | Yes | Yes | Yes | X, Y: categorical; Value: numeric |
| Treemap | Yes | Yes | — | — | Two-level hierarchy; Value: numeric |
| Sunburst | Yes | Yes | — | — | Two-level hierarchy; Value: numeric |
| Mean ± σ band | Yes | Yes | Yes | — | X: categorical; Y: numeric grouped |
| Mean ± σ bars | Yes | Yes | Yes | — | X: categorical; Y: numeric grouped |
| Density (KDE) | Yes | Yes | Yes | Yes | X: numeric; group optional |
| Violin Plot | Yes | Yes | Yes | Yes | X: categorical (opt.); Y: numeric |
| Radar / Spider | Yes | Yes | — | — | Multiple numeric fields per category |
| Pareto (80/20) | Yes | Yes | Yes | — | Category: 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:
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:
6.2 Skewness and Outliers
Adjusted Fisher-Pearson sample skewness:
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).
| Mode | Expression | Use Case |
|---|---|---|
| None | Raw values | 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 |
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():
frame_groups()partitions rows by the animate field value, ordered numerically or lexicographically.union_categories()merges all frames' categories into a stable axis order.align_values()pads missing categories with zeros.- Global numeric ranges are fixed across all frames so axes do not rescale.
- 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:
| Overlay | Computation | Style |
|---|---|---|
| Mean line | \(\bar{x} = \frac{1}{n}\sum x_i\) | Dashed line with label |
| Median line | Linear-interpolation median | Dotted line with label |
| ±1σ band | \([\bar{x} - s, \bar{x} + s]\) | Shaded region |
| IQR band | Shaded from Q1 to Q3 | Shaded region with label |
| Target value | User-entered constant | Solid 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
| Control | Description |
|---|---|
| Layer combo | QgsMapLayerComboBox (vector) or external file (CSV/XLSX/ODS/GPKG/DBF) |
| Selected features only | Uses only selected features; auto-refreshes on selectionChanged |
| Live refresh | Re-renders on layer edit; max 100,000 rows; guarded for deleted C++ objects |
Charts Tab Controls
| Control | Description |
|---|---|
| Engine | ECharts / Plotly / Vega-Lite / Matplotlib |
| Chart type | 17 types; unsupported types greyed per engine; field selectors adapt contextually |
| Field bindings | X, Y, Group, Value, Animate by (context-sensitive per type via _CONTROLS matrix) |
| Aggregation | Count / Sum / Mean / Median / Min / Max |
| Bins / Top-N / Sort | Context-sensitive; Top-N collapses remainder to "Other" |
| Stacked / Trend line | Checkboxes; trend = least-squares linear regression |
| Reference overlays | Mean, median, ±1σ band, IQR band, target value |
| Theme / Palette | 4 themes; 8 palettes + custom inline swatch editor |
| Animation speed | Slow (1600 ms) / Medium (900 ms) / Fast (450 ms) |
Map Diagrams Tab Controls
| Control | Description |
|---|---|
| Diagram type | Pie / Bar / Stacked bar / Text |
| Size (mm) | 3–60 mm, default 14 |
| Fields | Multi-select numeric field checkboxes |
| Normalisation | None / Min-max / Z-score / Log; warns for pie+Z-score |
| Apply / Remove | Sets or clears diagram renderer |
Labels Tab Controls
| Control | Description |
|---|---|
| Preset | Clean / Strong halo / Bold / Plain |
| Primary / Second field | QgsFieldComboBox per row |
| Decimals / Separator | −1–6; thousands separator checkbox |
| Prefix / Suffix / Case / Wrap | String; dropdown; spinbox |
| Advanced expression | Freeform QGIS expression (overrides formatting); validated |
| Live preview | Expression text + first feature evaluation |
15. Workflow Guide
- Click the 02viz toolbar icon to toggle the dock.
- Data: Select a vector layer or load external file. Optionally enable "selected features only" or "live refresh".
- 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.
- Diagrams: Switch to Diagrams tab. Check fields, choose type/size/normalisation. Click Apply to layer.
- Labels: Switch to Labels tab. Choose fields, configure formatting, verify in preview. Click Apply to layer.
- Explore: Click "Explore layer" for full statistical dashboard. Use Tile Picker to toggle tiles.
- Animation: Select an "Animate by" field. Chart renders with timeline controls. Optionally set speed.
- Presets: Save/load chart configurations. Fields remap by name across layers.
16. Technical Notes
- Pure-Python statistics: All aggregation, histogram, Pearson r, KDE (Silverman bandwidth), boxplot (linear-interpolation quantiles), skewness (adjusted Fisher-Pearson), outlier detection (Tukey 1.5×IQR), and min-max normalisation. Only the Matplotlib engine has optional Python dependencies.
- Data shaping: Group-by, pivot (category × series), heatmap matrix, treemap/sunburst hierarchies, Top-N with "Other" collapse, per-frame slicing for animation, bubble sqrt-scaling, least-squares trend line.
- Engine isolation: Every engine exposes only
build_html(spec) → str. Never touch Qt. Testable in pure Python. Usable for batch export. - WebView fallback: QtWebEngine → QtWebKit → system browser. Plotly/Vega-Lite on WebKit shows styled explainer page.
- Dock styling: Explicitly locked light theme for Qt5/Qt6 consistency.
- SmartModeler bridge: v0.14.0 adds
smartmodeler_suggest_chart(layer_id)for integration. Narrow, reviewed interface: no file paths, URLs, exports, or feature values exposed. - Accessibility: All buttons have
accessibleName/accessibleDescription;TabFocuspolicy. - QGIS compatibility: 3.28–4.99. Zero external Python dependencies. Bundled JS libraries inlined for fully offline operation.
17. Literature
- Wilkinson, L. (2005). The Grammar of Graphics (2nd ed.). Springer. DOI: 10.1007/0-387-28695-0
- Tufte, E. R. (1983). The Visual Display of Quantitative Information. Graphics Press.
- Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley.
- 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
- 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
- Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90–95. DOI: 10.1109/MCSE.2007.55
- 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
- Silverman, B. W. (1986). Density Estimation for Statistics and Data Analysis. Chapman & Hall. DOI: 10.1007/978-1-4899-3324-9
- Wong, B. (2011). Points of view: Color blindness. Nature Methods, 8(6), 441. DOI: 10.1038/nmeth.1618
- Juran, J. M. (1954). Universals in management planning and controlling. Management Review, 43(11), 748–761.