# 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:

\(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).

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()`:

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`:

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

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

- 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`; `TabFocus` policy.
- QGIS compatibility: 3.28–4.99. Zero external Python dependencies. Bundled JS libraries inlined for fully offline operation.

## 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.