# viz

> Generate interactive charts & maps from CSV data using [plotly](https://plotly.com). `viz smart` creates a [Data Schematic](../DATA_SCHEMATIC.md) — a *"[neuro-symbolic](https://en.wikipedia.org/wiki/Neuro-symbolic_AI)"* interactive rendering of a dataset's schema & statistics — picking appropriate visualizations using the dataset's statistics, frequency distributions, data dictionary & optional LLM metadata inferencing/classification, with automatic geocoding enrichment. Outputs self-contained, interactive HTML or static PNG/SVG/PDF/JPEG/WebP with the `viz_static` feature. ([Gallery](https://dathere.github.io/qsv/gallery.html))

**[Table of Contents](TableOfContents.md)** | **Source: [src/cmd/viz.rs](https://github.com/dathere/qsv/blob/master/src/cmd/viz.rs)** | [🪄](TableOfContents.md#legend "\"automagical\" commands that uses stats and/or frequency tables to work \"smarter\" & \"faster\".")[📇](TableOfContents.md#legend "uses an index when available.")[🏎️](TableOfContents.md#legend "multithreaded and/or faster when an index (📇) is available.")[👆](TableOfContents.md#legend "has powerful column selector support. See `select` for syntax.")[🤖](TableOfContents.md#legend "command uses Natural Language Processing or Generative AI.")[🌐](TableOfContents.md#legend "has web-aware options.")[🌎](TableOfContents.md#legend "has geospatial capabilities.")

<a name="nav"></a>
[Description](#description) | [Examples](#examples) | [Usage](#usage) | [Viz Options](#viz-options) | [Map Options](#map-options) | [Geo Options](#geo-options) | [Choropleth Options](#choropleth-options) | [Smart Options](#smart-options) | [Common Options](#common-options)

<a name="description"></a>

## Description [↩](#nav)

Generate charts/maps from CSV data using the plotly charting library.

Produces a self-contained, interactive HTML chart (the plotly.js runtime is embedded,
so charts work offline; map basemaps fetch their tiles over the network at view time
unless the `white-bg` style is used). Set the QSV_VIZ_CDN environment variable to load
plotly.js from its CDN instead, shrinking the page by ~1.9MB at the cost of needing
network access to view it. By default the embedded plotly.js is gzip-compressed
(~1.9MB vs ~4.8MB) and inflated in-browser, which requires a browser with
DecompressionStream support (Chrome/Edge 80+, Firefox 113+, or Safari 16.4+);
set QSV_VIZ_NO_COMPRESS for plain-text, uncompressed HTML that also works on
older browsers.
Titles and labels are rendered as plain text - LaTeX (e.g. `$\alpha$`) is not typeset.
With a qsv build that includes the `viz_static` feature, charts can also be exported as
static PNG/SVG/PDF/JPEG/WebP images (this requires a Chromium/Firefox browser at
runtime - a webdriver is auto-managed by plotly).

The output format is inferred from the --output file extension (.html is the default).
Interactive HTML is written to stdout when --output is not given; image formats always
require --output. Use --open to view the result in your default browser/viewer.

Progress is shown on stderr by default: a spinner with per-phase status messages (loading
statistics, inferring the data dictionary, computing correlations, rendering, etc.). It is
auto-hidden when stderr is not a terminal (e.g. piped or redirected). Set the QSV_PROGRESSBAR
environment variable to a falsy value (0/false/off) to disable it.

Chart types (subcommands):

```text
smart       Auto-dashboard (Data Schematic). Picks an appropriate chart per
            column from the dataset's statistics & frequency distribution
            (no --x/--y needed).
bar         Bar chart.        --x = category column, --y = value column.
line        Line chart.       --x = x column, --y = y column.
scatter     Scatter plot.     --x = x column, --y = y column.
scatter3d   3D scatter plot.  --x, --y, --z = three numeric columns.
histogram   Distribution.     --x = numeric column to bin.
box         Box plot.         --y = value column, optional --x = group column.
violin      Violin plot: a box plot plus a KDE density curve revealing the
            distribution's shape (modes, shoulders). Same inputs as box
            (--y = value column, optional --x = group column).
pie         Proportions.      --x = label column, optional --y = value column.
funnel      Stage-by-stage drop-off. --x = stage column, optional --y = value
            column (counts stage occurrences when omitted). Stages keep the
            order they first appear in the file, so the rows define the
            pipeline; plotly labels each band with its conversion from the
            previous stage.
heatmap     Color grid. Correlation matrix of numeric columns (default; an
            optional column subset via --cols), or a category x category pivot
            with --x/--y/--z.
contour     2D density contour of two numeric columns (--x and --y), binned
            into a grid (--bins controls the grid resolution).
candlestick Financial OHLC.   --x = date column, plus --ohlc-open/--high/--low/--close.
ohlc        Financial OHLC bars (same inputs as candlestick).
sankey      Flow diagram.     --source, --target, optional --value column.
radar       Polar/radar chart of numeric --cols, optional --series per trace.
treemap     Part-to-whole hierarchy as nested tiles. --cols = 2+ dimension
            columns (levels), optional --value and --agg.
sunburst    Part-to-whole hierarchy as concentric rings (same inputs as
            treemap). Better for deeper hierarchies.
icicle      Part-to-whole hierarchy as stacked bars per level (same inputs as
            treemap). Level-aligned; good for deep, wide hierarchies.
splom       Scatter-plot matrix: every pair of numeric --cols plotted against
            each other in a grid, with each column's distribution on the
            diagonal. Good for spotting correlations across many numeric
            columns at once (default: all numeric columns).
parcats     Parallel categories: ribbons showing how rows flow across the
            categorical --cols (best with 3-4). Complements sankey (which
            takes 2 columns) for higher-dimensional categorical relationships.
map         Geographic point map (or --density heatmap) on tile basemaps.
            Pick the coordinate columns with the lat/lon options below.
geo         Geographic point map on a projection basemap (coastlines/land/
            countries; no tiles, no token). Uses the same lat/lon options
            as `map`, plus --projection. Good for global/country-scale data.
choropleth  Filled-region map: color whole regions (countries, US states, or
            custom GeoJSON areas) by a value. --locations names the region-code
            column, --value/--agg the measure (row counts if omitted). Defaults
            to a token-free projection basemap; --map switches to MapLibre tiles.
```

`qsv viz smart` builds a DATA SCHEMATIC - a single, self-contained rendering of a
dataset's schema and statistics in which every claim shown is checkable against the
data it describes. The format is defined in docs/DATA_SCHEMATIC.md and is
tool-neutral; what `viz smart` produces is a "qsv Schematic".

A schematic is the drawing form of a SCHEMA - and `viz smart` already emits a
`.schema.json` - so the name is descriptive rather than decorative. Where a data
dictionary lists fields one at a time, a schematic shows components AND HOW THEY
CONNECT: correlation, process order, hierarchy, temporal pacing, spatial pairing.
It alloys two ways of knowing a dataset. The DETERMINISTIC half (statistics,
heuristics and algorithms - reproducible, offline, no tokens) picks the panels and
scales the axes; the SEMANTIC half (an optional LLM-inferred Data Dictionary, via
the --dictionary option) supplies what the fields MEAN - labels, roles and units,
all correctable by a human Data Steward. Both halves show their work.

The schematic is a one-page grid of subplots, reusing qsv's stats and frequency
caches (the first run computes & caches stats; later runs are fast). It auto-picks
panels, so no --x/--y is needed:  

Per-column panels (flow in the grid below the overview rows, see --grid-cols):  
- continuous numeric -> box plot (quartiles from the stats cache; sample
points overlaid by a size heuristic, see --box-points)
- low-cardinality / boolean -> frequency bar chart
- ID-like (near-unique) and all-empty columns are skipped

Overview panels (each leads the Data Schematic on its own full-width row):  
- KPI overview row (leads the Data Schematic when the dataset has headline numeric
measures): a strip of "big number" tiles, one per headline measure (summed
for extensive quantities, averaged for intensive ones). A measure tile
becomes a GAUGE when the dictionary supplies a validated `x-qsv.gauge_range`
that contains the value, and gains a "vs target" DELTA when it supplies an
`x-qsv.target` (see --dictionary). Omitted for image exports and for datasets
with no headline measure. (Overall dataset completeness - the share of
non-empty cells - is a quiet "Completeness:" line in the header metadata
table, not a KPI tile.)
- pipeline panel, when the dictionary DECLARES one (see --dictionary). Which
columns are process stages, and in which direction, is semantics rather than
a statistic - no column-name vocabulary settles it and no statistic does
either - so the panel is drawn ONLY from an explicit declaration, never
guessed. Both encodings are supported: stages held in separate measure
columns, and stages held as values of one category column. Costs one extra
data pass over the declared stages only.
The declaration fixes WHICH columns and in WHAT order; the numbers decide the
FORM. A funnel's band widths are a containment claim, so one is drawn only
while the stage totals never grow. If any stage outruns the one before it, the
same declaration is drawn as a BRIDGE instead: the signed difference between
consecutive totals, each step labelled as the arithmetic difference it is
rather than as a flow. A funnel there would render a band wider than the one
above it, asserting the opposite of the data. The subtitle says which form was
used and why.
Stage order is the declared order and is never re-sorted by size. For the
column encoding, row-wise containment (does each stage nest inside the one
before it?) is MEASURED and disclosed in the subtitle - separately from the
form, since rows can overrun while the totals still shrink, or nest while the
totals grow. Totals sum over the rows complete across every declared stage,
so they do NOT match `stats.sum`; the subtitle always discloses that
denominator. See also the standalone `qsv viz funnel` chart type, which takes
its stage order from the file and needs no dictionary.
- correlation heatmap, when 2+ continuous numeric columns exist (one extra
data pass for Pearson correlations). If the strongest pair is at least
moderately correlated, a drill-down is added beside it: a scatter (or a 2D
density contour for large, overplotting datasets); with 3+ numeric columns,
a 3D scatter of the strongest triple is added too.
- time-series line, when an auto-detected date/datetime column and a
continuous numeric column both exist.
- geographic map, when a latitude/longitude pair is detected:
    - HTML uses a MapLibre tile map for a local extent, or an offline
      ScatterGeo projection world-overview for continental/global data.
    - static image export uses an offline ScatterGeo fit to the data extent
      (US-spanning data uses albers-usa); tile maps and 3D panels stay
      HTML-only, as tile maps need network tiles.
    - geographic outliers (points beyond the Tukey far-out fence of
      distances from the cluster centroid) get a distinct marker and are
      excluded from the spatial extent; the map zooms to the core, with a
      dotted no-fill box marking the full extent and (in HTML) Core/Full
      extent buttons. Outliers within the core's jurisdiction don't trigger
      the extent call-out.
    - with the `geocode` feature, the core extent (4 corners + center) is
      reverse-geocoded against the local Geonames index and drawn as a
      labeled bounding box with a location summary (e.g. "New York & New
      Jersey, United States"); outliers are called out with their count and
      jurisdiction. HTML points reveal city/state/country on hover (static
      exports omit it). The first run may download the index (~13MB, cached
      in ~/.qsv-cache); offline, the map renders without the overlay.
    - extents spanning the antimeridian (>180 degrees of longitude) are
      skipped.


<a name="examples"></a>

## Examples [↩](#nav)

> Data Schematic for a dataset, opened in the browser

```console
qsv viz smart data.csv --open
```

> Data Schematic, at most 6 panels in a 3-column grid, top-5 categories per bar

```console
qsv viz smart data.csv --max-charts 6 --grid-cols 3 --limit 5 -o dashboard.html
```

> Bar chart of fruit prices, opened in the browser

```console
qsv viz bar fruits.csv --x Fruit --y Price --title "Fruit prices" --open
```

> Aggregate (sum) sales by region into a bar chart

```console
qsv viz bar sales.csv --x region --y amount --agg sum -o sales.html
```

> Scatter plot with a separate series (trace) per category

```console
qsv viz scatter data.csv --x age --y income --series gender -o scatter.html
```

> Bubble scatter: marker size by population, marker color by a numeric score

```console
qsv viz scatter data.csv --x gdp --y life_exp --size population --color score -o bubble.html
```

> Gapminder bubble animation: one bubble per region moving over time

```console
qsv viz scatter regions.csv --x gdp_index --y wellbeing_index --size population_m --series region --slider month_date -o gapminder.html
```

> Histogram of a numeric column with 30 bins

```console
qsv viz histogram data.csv --x value --bins 30 -o hist.html
```

> Box plot of a value column grouped by a category, exported to PNG (needs viz_static)

```console
qsv viz box data.csv --y measurement --x group -o box.png
```

> Box plot with every sample point overlaid (jittered) instead of just the outliers

```console
qsv viz box data.csv --y measurement --box-points all -o box.html
```

> Violin plot (KDE density curve + inner box) of a value column grouped by a category

```console
qsv viz violin data.csv --y measurement --x group -o violin.html
```

> Pie chart of category proportions (counts), as a donut

```console
qsv viz pie data.csv --x category --donut -o pie.html
```

> Funnel of a pipeline whose stages are ROWS (stage column + amount column)

```console
qsv viz funnel pipeline.csv --x stage --y amount -o funnel.html
```

> Correlation heatmap over all numeric columns

```console
qsv viz heatmap data.csv -o corr.html
```

> Heatmap pivot: average value per (region x product)

```console
qsv viz heatmap sales.csv --x region --y product --z amount -o pivot.html
```

> Candlestick chart from a date column and OHLC price columns

```console
qsv viz candlestick prices.csv --x date --ohlc-open open --high high --low low --close close -o ohlc.html
```

> Sankey flow diagram of source -> target weighted by value

```console
qsv viz sankey flows.csv --source from --target to --value weight -o sankey.html
```

> Radar chart comparing numeric metrics, one trace per team

```console
qsv viz radar teams.csv --cols speed,power,range,accuracy --series team -o radar.html
```

> Point map of earthquakes, marker color by magnitude and size by depth

```console
qsv viz map quakes.csv --lat lat --lon lon --color magnitude --size depth -o map.html
```

> Density heatmap of the same points, on a light Carto basemap

```console
qsv viz map quakes.csv --lat lat --lon lon --density --style carto-positron -o heat.html
```

> 3D scatter of three numeric columns, colored by a fourth

```console
qsv viz scatter3d data.csv --x length --y width --z height --color weight -o scatter3d.html
```

> 2D density contour of two numeric columns with a 40x40 grid

```console
qsv viz contour data.csv --x height --y weight --bins 40 -o contour.html
```

> Projection map of earthquakes (token-free), marker color by magnitude

```console
qsv viz geo quakes.csv --lat lat --lon lon --color magnitude --projection natural-earth -o geo.html
```

> Treemap of part-to-whole sales by region then category, sized by amount

```console
qsv viz treemap sales.csv --cols region,category --value amount --agg sum -o treemap.html
```

> Sunburst of a deep 3-level web-traffic hierarchy, sized by row count

```console
qsv viz sunburst web.csv --cols source,campaign,landing_page -o sunburst.html
```

> Choropleth coloring countries (ISO-3 codes) by a summed measure

```console
qsv viz choropleth gdp.csv --locations iso3 --value gdp --agg sum -o choropleth.html
```

> US-state choropleth of row counts per state (2-letter state codes)

```console
qsv viz choropleth orders.csv --locations state --location-mode usa-states -o states.html
```

> Custom GeoJSON regions on a MapLibre basemap, matched by a feature id

```console
qsv viz choropleth counties.csv --locations fips --value pop --map --geojson counties.json --feature-id-key id -o counties.html
```

> Reverse-geocode lat/lon points to ISO-3 codes, then count per country (needs geocode feature)

```console
qsv viz choropleth stops.csv --geocode --lat lat --lon lon -o by_country.html
```

> Point-in-polygon: bin lat/lon points into custom GeoJSON regions by count (no geocode)

```console
qsv viz choropleth quakes.csv --lat lat --lon lon --geojson prefectures.geojson --feature-id-key properties.id -o by_pref.html
```

For more examples, see [tests](https://github.com/dathere/qsv/blob/master/tests/test_viz.rs).

See also <https://github.com/dathere/qsv/wiki/Visualization>

<a name="usage"></a>

## Usage [↩](#nav)

```console
qsv viz smart       [options] <input>
qsv viz bar         [options] <input>
qsv viz line        [options] <input>
qsv viz scatter     [options] <input>
qsv viz scatter3d   [options] <input>
qsv viz histogram   [options] <input>
qsv viz box         [options] <input>
qsv viz violin      [options] <input>
qsv viz pie         [options] <input>
qsv viz funnel      [options] <input>
qsv viz heatmap     [options] <input>
qsv viz contour     [options] <input>
qsv viz candlestick [options] <input>
qsv viz ohlc        [options] <input>
qsv viz sankey      [options] <input>
qsv viz radar       [options] <input>
qsv viz map         [options] <input>
qsv viz geo         [options] <input>
qsv viz choropleth  [options] <input>
qsv viz treemap     [options] <input>
qsv viz sunburst    [options] <input>
qsv viz icicle      [options] <input>
qsv viz splom       [options] <input>
qsv viz parcats     [options] <input>
qsv viz --help
```

<a name="viz-options"></a>

## Viz Options [↩](#nav)

| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Option&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Type | Description | Default |
|--------|------|-------------|--------|
| &nbsp;`‑x,`<br>`‑‑x`&nbsp; | string | Column for the x-axis / category / bin / group. |  |
| &nbsp;`‑y,`<br>`‑‑y`&nbsp; | string | Column for the y-axis / value. |  |
| &nbsp;`‑z,`<br>`‑‑z`&nbsp; | string | The z column: a heatmap pivot value (with --x and --y), or the third numeric axis for scatter3d. |  |
| &nbsp;`‑‑cols`&nbsp; | string | Columns to use. For heatmap: numeric columns for the correlation matrix (default: all numeric). For radar: the numeric axes to plot. For treemap/sunburst/icicle: the categorical dimensions that form the hierarchy levels, outermost first (e.g. region,category,subcategory). For splom: the numeric columns to cross-plot (default: all numeric). For parcats: the categorical dimensions to flow across (best with 3-4). |  |
| &nbsp;`‑‑series`&nbsp; | string | Column to split into multiple series (one trace per distinct value). Applies to bar, line, scatter, scatter3d, radar, map and geo. |  |
| &nbsp;`‑‑color`&nbsp; | string | For scatter/scatter3d/map/geo: a numeric column to encode as marker color (a continuous colorscale with a colorbar). For categorical coloring, use the --series option instead. Cannot be combined with --series. In map density mode, this column is the heatmap weight. |  |
| &nbsp;`‑‑size`&nbsp; | string | For scatter/scatter3d/map/geo: a numeric column to encode as marker size, producing a bubble chart (values are rescaled to a readable pixel range). Cannot be combined with --series, EXCEPT on an animated scatter (see the --slider option), where the --series column names the entity and this column is the third data variable. In map density mode, this column is the heatmap weight. |  |
| &nbsp;`‑‑donut`&nbsp; | flag | Render a pie chart as a donut (with a center hole). |  |
| &nbsp;`‑‑ohlc‑open`&nbsp; | string | Open-price column for candlestick/ohlc charts. |  |
| &nbsp;`‑‑high`&nbsp; | string | High-price column for candlestick/ohlc charts. |  |
| &nbsp;`‑‑low`&nbsp; | string | Low-price column for candlestick/ohlc charts. |  |
| &nbsp;`‑‑close`&nbsp; | string | Close-price column for candlestick/ohlc charts. |  |
| &nbsp;`‑‑source`&nbsp; | string | Source node column for a sankey diagram. |  |
| &nbsp;`‑‑target`&nbsp; | string | Target node column for a sankey diagram. |  |
| &nbsp;`‑‑value`&nbsp; | string | Flow value column for a sankey diagram. When omitted, each row counts as a flow of 1. For treemap/sunburst/icicle: a numeric measure summed per sector (when omitted, each row counts as 1). |  |
| &nbsp;`‑‑sankey‑value‑order`&nbsp; | flag | Order sankey nodes by total flow (largest at the top of each column). By DEFAULT nodes use plotly's crossing-minimizing "snap" order, which packs the diagram more compactly; this opts into the flow-ranked initial ordering instead. Either way the rendered chart carries an on-screen "node order" button to flip between the two layouts, and link ribbons are always colored by their source node. Applies to the `sankey` chart and the `smart` Data Schematic flow panel. |  |
| &nbsp;`‑‑bins`&nbsp; | integer | Number of bins. For histogram: bins along the x-axis (default: auto). For contour: the per-axis resolution of the density grid (default: 20). |  |
| &nbsp;`‑‑agg`&nbsp; | string | For bar/line, aggregate the y values when the x value repeats. One of: sum, mean, count, min, max. For treemap/sunburst/icicle, only additive aggregations apply: count (default) or sum (requires --value). For a bubble animation (see --slider), collapses each entity x frame cell to a single bubble; defaults to mean (the cell centroid). count is rejected there, since a row count is not a position. |  |
| &nbsp;`‑‑box‑points`&nbsp; | string | Which sample points to draw alongside a box. Reading the raw values lets plotly render true Tukey whiskers (1.5*IQR) with the points beyond the fences as outliers. One of: outliers (only the outliers), all (every point, jittered), suspected (mark suspected outliers), none (no points, but still real Tukey whiskers). For `viz box` the default is outliers. For `viz smart` this flag OVERRIDES the default size-based heuristic, which overlays all points on a column with few non-null values (<=1,000) — unless the Data Schematic has more than 8 panels, where all-points cells turn to noise, so the overlay drops to outliers-only — and only the outliers for a medium one (<=10,000 values). Above that, a column that HAS outliers shows them as points on a precomputed quartile box (a single pass collects only the out-of-fence values, capped); a column with no outliers stays a fast cache-only quartile summary with no data re-scan. An explicit mode is applied to every box panel (one batched pass to read the values), except `none`, which keeps the cache-only box for BOX panels. A smart violin panel still renders under `none`, just with no sample points — its KDE needs the values anyway; combine with the violin option's `off` mode for the cache-only escape hatch. The same modes also pick the sample points drawn beside a violin, for `viz violin` and for the violin panels of `viz smart` — though a smart violin panel skips the size-based `all` upgrade (its KDE silhouette already shows the distribution) and overlays only the outliers unless an explicit mode is given. |  |

<a name="map-options"></a>

## Map Options [↩](#nav)

| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Option&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Type | Description | Default |
|--------|------|-------------|--------|
| &nbsp;`‑‑lat`&nbsp; | string | Latitude column for a map (decimal degrees, -90 to 90). |  |
| &nbsp;`‑‑lon`&nbsp; | string | Longitude column for a map (decimal degrees, -180 to 180). |  |
| &nbsp;`‑‑text`&nbsp; | string | Column whose value labels each point on hover. |  |
| &nbsp;`‑‑density`&nbsp; | flag | Render a density heatmap (DensityMap) instead of points. Weighted by the --color or --size column when given, else by a uniform weight. Cannot be combined with --series. |  |
| &nbsp;`‑‑style`&nbsp; | string | MapLibre basemap style (all render without an access token): open-street-map (the default), carto-positron, carto-darkmatter, carto-voyager, white-bg, basic, streets, outdoors, light, dark, satellite, satellite-streets. The three carto styles also have label-free "-nolabels" variants (e.g. carto-positron-nolabels) that keep basemap place names from competing with a dense data overlay. | `open-street-map` |

<a name="geo-options"></a>

## Geo Options [↩](#nav)

| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Option&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Type | Description | Default |
|--------|------|-------------|--------|
| &nbsp;`‑‑projection`&nbsp; | string | Map projection for `viz geo`. One of: natural-earth (the default), mercator, orthographic, equirectangular, albers-usa, robinson, winkel-tripel, mollweide, hammer, azimuthal-equal-area. `viz geo` also reuses the lat, lon, text, color, size and series options from `map`. | `natural-earth` |

<a name="choropleth-options"></a>

## Choropleth Options [↩](#nav)

| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Option&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Type | Description | Default |
|--------|------|-------------|--------|
| &nbsp;`‑‑locations`&nbsp; | string | Column holding the region key for each row (an ISO-3 country code, a 2-letter US state code, a country name, or a GeoJSON feature id, per --location-mode). With --geocode, this instead names a place-name column to forward-geocode into region codes. |  |
| &nbsp;`‑‑location‑mode`&nbsp; | string | How --locations values are matched to regions. One of: iso3 (the default, ISO-3166-1 alpha-3 country codes), usa-states (2-letter US state codes), country-names (full country names), geojson-id (match a --geojson feature id). | `iso3` |
| &nbsp;`‑‑color‑scale`&nbsp; | string | Colorscale for the region fill. One of: viridis (the default), cividis, greys, greens, blues, reds, ylgnbu, ylorrd, bluered, rdbu, portland, electric, jet, hot, blackbody, earth, picnic, rainbow. | `viridis` |
| &nbsp;`‑‑map`&nbsp; | flag | Render on a token-free MapLibre tile basemap (a ChoroplethMap) instead of the default projection basemap. Requires --geojson; feature-id-key defaults to id. Reuses --style for the basemap. |  |
| &nbsp;`‑‑geojson`&nbsp; | string | Custom region polygons as a local file path or an http(s) URL to a GeoJSON FeatureCollection. May also be a shortcut name defined in the QSV_GEOJSON_SHORTCUTS env var (a JSON map of name to {path, id}); the shortcut's id sets --feature-id-key when you don't pass one. The source is validated up front. Required for --map, and for the geojson-id location mode. Also enables point-in-polygon binning: with --lat/--lon (and without --geocode), each row's point is binned into the region whose polygon contains it (exact, no geocoding) and colored by --value/--agg or counts. In `viz smart`, it also overlays the region boundaries on the map, labelling each with its --feature-name-key value (falling back to its id) — as on-map text on the projection/static map, or as a centroid hover marker on the interactive tile map (which culls on-map text). Labels are omitted above 60 regions. |  |
| &nbsp;`‑‑feature‑id‑key`&nbsp; | string | Property path in each GeoJSON feature whose value matches an entry in the locations column, or that labels each binned region (e.g. id, properties.fips). | `id` |
| &nbsp;`‑‑feature‑name‑key`&nbsp; | string | GeoJSON property path whose value is shown as the human-readable region label in choropleth hover (e.g. properties.name). When omitted, common name keys are auto-detected; falls back to the feature id when absent. |  |
| &nbsp;`‑‑geocode`&nbsp; | flag | Derive the region codes by reusing qsv's geocode engine (needs a build with the geocode feature). Either reverse-geocode the lat/lon points, or forward-geocode the locations name column. Only valid with location modes iso3 or usa-states. `viz choropleth` also reuses --value, --agg, --style and the lat/lon options. |  |
| &nbsp;`‑‑no‑snap`&nbsp; | flag | For point-in-polygon binning (lat/lon points binned into a custom GeoJSON without geocoding): do not snap at all — drop every point that falls outside every region. By default an outside point instead snaps to its nearest region when within the snap-distance limit (see --snap-max-dist). Applies to both the `viz choropleth` command and the `viz smart` GeoJSON choropleth panel. A stderr note reports coverage either way; each snapping region's hover tallies the points it absorbed from outside, and snapped or dropped points are reported in a note beneath the map (or in the smart panel's title). |  |
| &nbsp;`‑‑snap‑max‑dist`&nbsp; | float | For point-in-polygon binning: the farthest (in km) an outside point may snap to a region's boundary; points with no region within this distance are dropped. Distance is an equirectangular km approximation. When omitted, the cap is auto-derived from the GeoJSON's median region size (10% of the median bbox diagonal) and the lat/lon coordinate precision (coarse, few-decimal coordinates raise the cap to cover their quantization error), clamped to 0.1-100 km; a fixed 10 km is used only when neither signal is available. Applies to both the `viz choropleth` command and the `viz smart` GeoJSON choropleth panel; the coverage notes state the cap applied and how it was derived. Pass a large value for effectively unbounded snapping; cannot be combined with --no-snap. |  |

<a name="smart-options"></a>

## Smart Options [↩](#nav)

| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Option&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Type | Description | Default |
|--------|------|-------------|--------|
| &nbsp;`‑‑max‑charts`&nbsp; | integer | Maximum number of panels in the Data Schematic. 0 (the default) means auto: draw every eligible column (up to 64), for both HTML and static image export (png/svg/pdf/...). Up to 8 cartesian panels render as one typed subplot grid; beyond 8, HTML switches to an inline-div grid of independent plots, and static image export uses domain-positioned axes to fit them in one image. Set a positive <n> to cap the panel count instead. Eligible columns beyond the cap are reported but not drawn. | `0` |
| &nbsp;`‑‑grid‑cols`&nbsp; | integer | Number of columns in the Data Schematic grid for the per-column distribution panels. Overview panels (map/geo, correlation, time-series) always span the full width. | `2` |
| &nbsp;`‑‑preview‑threshold`&nbsp; | integer | Row threshold for the interactive data viewer drawer of HTML dashboards. Next to the row count in the Data Schematic metadata, an "(Explore)" link opens the underlying table in a bottom drawer (with global, per-column and builder search) when the dataset has at most <n> rows and ALL rows are embedded in the page; above <n>, only the first <n> rows are embedded and the link reads "(Preview)". Note that embedded rows grow the HTML file (and browser memory) in proportion to rows x columns. Set to 0 to disable the data viewer entirely (no link, no embedded data). Only affects `smart` HTML output. | `50000` |
| &nbsp;`‑‑heatmap‑density`&nbsp; | integer | For the `viz smart` map panel: at or above <n> mappable points, draw the core as a density heatmap (DensityMap) instead of markers. The heatmap keeps per-point hover (coordinates, plus the point's label). Defaults to 0 (disabled): dense point maps instead open as individual points with an in-map "Clusters/Points" toggle (collapse the dense areas into interactive count bubbles on demand) rather than aggregating into a static heat surface. Set a positive <n> to opt back into the heatmap above that point count. | `0` |
| &nbsp;`‑‑cluster`&nbsp; | string | For the `viz smart` map panel: whether to offer an in-map "Clusters/Points" toggle that collapses dense points into native MapLibre count bubbles (which expand back into individual, hoverable points on zoom-in). Hovering a bubble shows its point count; clicking it zooms to where that cluster breaks apart. The map always OPENS as individual points; the toggle switches clustering on. One of: auto, on, off. "auto" (the default) offers the toggle for a plain-marker core (no density heatmap, no bubble-size measure) once it reaches 1,000 points. "on" offers it whenever the core draws as markers, regardless of point count or a bubble-size measure. "off" omits the toggle, so points are always drawn plainly. Only affects `smart`. | `auto` |
| &nbsp;`‑‑photos`&nbsp; | flag | For the `viz smart` map panel: when a column holds image URLs (detected from the stats cache - an http(s) value ending in a common image extension), embed each point's image URL so that dwelling on a map point for 2 seconds opens that row's photo in a small preview card beside the pointer (arrow keys page through a row with several photos; Escape dismisses the card). Points that have photos say so in their hover label. When the image host allows cross-origin reads, fetched images are cached in the browser's IndexedDB, so re-dwelling a point or reloading the page serves them locally; otherwise the preview falls back to a normal image load served by the ordinary browser cache. OFF by default, and deliberately so: images load from whatever third-party host the DATA names, so enabling this makes everyone who opens the Data Schematic request those URLs directly (revealing their IP to that host). Nothing is fetched until a viewer dwells. Embedding every mapped point's URLs also grows the HTML. Applies to HTML output only - static image exports (PNG/SVG/PDF) are unaffected - and only on the tile-basemap map panel. Only affects `smart`. |  |
| &nbsp;`‑‑limit`&nbsp; | integer | Top-N categories per frequency bar chart. | `10` |
| &nbsp;`‑‑no‑nulls`&nbsp; | flag | Omit the "(NULL)" bar (empty cells) from frequency bar charts. By default `viz smart` shows a "(NULL)" bar, like `qsv frequency`. |  |
| &nbsp;`‑‑no‑other`&nbsp; | flag | Omit the "Other (N)" aggregate bar from frequency bar charts. It collects the categories beyond --limit (N = how many distinct categories were rolled up) and is shown by default. When just ONE category is left over, it is charted under its own name instead of as an opaque "Other (1)" bar. |  |
| &nbsp;`‑‑smarter`&nbsp; | flag | Before building the Data Schematic, run `qsv moarstats --advanced` to enrich the stats cache with distribution-shape statistics (bimodality, entropy, skewness, outlier share, Gini). This unlocks histograms for bimodal columns, frequency bars for concentrated high-cardinality columns, skew/outlier hints on box panels, and Lorenz curves for the most unequal additive measures (high Gini). Costs one extra pass over the data and writes <stem>.stats.csv, its sidecars, and an .idx index (like running `qsv moarstats` manually). On geocode-enabled builds it also enriches map point hovers with the US FIPS code and annotates the spatial-extent summary with the country's continent; the county is always shown in map hovers, with or without --smarter. Only affects `smart`. Applied only with default parsing; inputs using --no-headers or a custom --delimiter fall back to the standard Data Schematic. |  |
| &nbsp;`‑‑hierarchy‑style`&nbsp; | string | For `smart`, the chart used for the categorical part-to-whole hierarchy panel (built when 2+ low-cardinality dimensions exist). One of: auto (default), treemap, sunburst, icicle. auto follows best practice — a treemap for a shallow 2-level hierarchy (accurate size comparison) and a sunburst for a deep 3-level one (parent child structure); icicle is an opt-in level-aligned alternative. Only affects `smart`. |  |
| &nbsp;`‑‑dictionary`&nbsp; | string | Use a describegpt Data Dictionary to guide panel selection from each field's semantic role/concept (falling back to its content type) instead of relying on column statistics alone: dimensions and numeric codes (ward, census_tract, zone) become bars, measures get box/correlation/trend panels, date/datetime columns feed the time-series panel (not noisy frequency bars), identifiers / PII / free-text are skipped, and lat/lon feed the map. Field labels are shown as panel subtitles beneath the field-name titles. Columns the dictionary cannot classify still use the statistical heuristic. <src> is one of: "infer" to run describegpt on the input now (with description, infer-content-type, two-pass and jsonschema output; requires an LLM configured) and use its output; or a path to an existing describegpt dictionary file (jsonschema or json). With "infer", the generated dictionary is saved beside the input as <stem>.schema.json so you can fine-tune it; if that file already exists, it is reused as-is (skipping the LLM) - edit it to fine-tune, or delete it to force a fresh re-infer. Set QSV_VIZ_DICT_FRESH=1 to ignore an existing sidecar and bypass describegpt's completion cache, forcing a genuinely fresh inference that overwrites the sidecar on success. Generation/read failures soft-fall back to the stats-only Data Schematic. The dictionary also drives the KPI overview row via two optional per-field hints in a property's "x-qsv" object (edit them in the saved schema to fine-tune). A "gauge_range" of [min, max] on a continuous numeric measure renders its KPI tile as a GAUGE on that canonical scale (e.g. [0,1] for a ratio, [0,100] for a percent); qsv keeps it only when the observed data lies within the range, so a mis-scaled range can't draw a misleading dial, and "infer" emits it for canonical-scale measures. A "target" number on a measure renders a "vs target" DELTA against that goal (value minus target) - a GOAL you supply, never a fabricated prior-period baseline, so "infer" never emits it; hand-author it. The dictionary is also the ONLY source of the pipeline panel (drawn as a funnel while the stage TOTALS never increase, otherwise as a bridge; row-wise containment is measured & disclosed in the subtitle, but does not decide the form), declared in the dataset-level "x-qsv" object as a "relationships" entry with "kind": "pipeline". Two encodings, both hand-editable: stages as COLUMNS - "members" lists the stage columns in process order, WIDEST/UPSTREAM FIRST (note this is the opposite direction from a "kind":"ordered" group, which ascends), e.g. {"kind":"pipeline", "members":["planned_amt","committed_amt","spent_amt"]} stages as ROW VALUES - "stage_column" names the category column, "stages" lists its values in process order, and an optional "value_column" names the measure to sum per stage (omit it to count rows), e.g. {"kind":"pipeline","members":["stage","revenue"], "stage_column":"stage", "stages":["Impression","Click","Lead","Conversion"], "value_column":"revenue"} Declared order is authoritative and is never re-sorted by size, so a stage that outruns its predecessor stays visible instead of being quietly reordered away - it switches the panel to a bridge rather than drawing a funnel that widens. A declaration naming a missing column, or a stage that is an average/rate rather than a summable amount, is skipped with a note rather than erroring. Only affects `smart`. |  |
| &nbsp;`‑‑dictionary‑context`&nbsp; | string | Path to a file with extra context about the dataset (a glossary, README, data dictionary, PDF, etc.) forwarded to describegpt as --context-file when `--dictionary infer` generates the dictionary. Better context yields better role/concept/label/grain tags, hence a better Data Schematic. Ignored unless `--dictionary infer` is used (it does not apply when reading an existing dictionary file). Only affects `smart`. |  |
| &nbsp;`‑‑dict‑info`&nbsp; | flag | When a usable Data Dictionary is available (per --dictionary), add a "Data Dictionary" link beneath the Data Schematic title and an info icon on each panel title: hovering shows that column's dictionary description; clicking opens a human-friendly rendering of the dictionary in a side drawer NEXT TO the plots (embedded in the Data Schematic file - no extra file is written), scrolled to and highlighting that column's entry. The drawer is open by default on load and can be dismissed with its close button or Esc. The dictionary page carries a role-tinted table of contents and per-column "View chart" links back to the panels, plus a row of download buttons: the dictionary itself as JSON Schema, the frequency counts the Data Schematic actually charted, and every generated sidecar this run read (the stats cache and its metadata, the frequency cache when it was reused, and the bivariate stats CSV when freshly written). A sidecar qsv wrote but viz never read - the human-readable <stem>.stats.csv - is NOT offered, since nothing can show it describes the same computation the Data Schematic used. Every file is BUNDLED into the HTML, so anyone you send the Data Schematic to can download them with no access to your machine; absolute local paths are stripped from the embedded metadata so sharing a Data Schematic doesn't disclose your directory layout. Sidecars over 4 MB are skipped with a note. The drawer's popout button opens the same document in its own browser tab instead (needs a browser that allows user-initiated pop-ups). HTML output only; ignored with a note when no dictionary is available or when exporting an image. Only affects `smart`. |  |
| &nbsp;`‑‑dataset‑pid`&nbsp; | string | A persistent identifier (PID) for the dataset - typically a full URL such as a DOI (<https://doi.org/10.1234/abc>) or other citable link. When set, a "PID" row is added to the metadata table at the top of the Data Schematic. http(s) and mailto values become a clickable link (opened in a new tab); any other scheme is shown as plain text rather than linked. HTML output only. Only affects `smart`. |  |
| &nbsp;`‑‑bivariate`&nbsp; | flag | Add two pairwise-association overview panels driven by `qsv moarstats --bivariate`: a normalized mutual information (NMI) heatmap over every column pair (works for numeric AND categorical columns, unlike the Pearson-only correlation heatmap), plus a ranked "top relationships" bar of the strongest pairs when there are more than 8 chartable columns. The ranked bar (not the heatmap, which still shows every pair) requires a pair's co-occurring row count to be at least 10% of the best-supported pair's, so a technically-perfect NMI from two sparsely-populated columns that only ever co-occur in a narrow row slice can't crowd out a more broadly meaningful association; each bar's hover shows its co-occurring row count. A numeric pair whose Spearman rank correlation diverges sharply from its Pearson correlation is flagged "nonlinear" in the hover. Identifier / PII / free-text columns (per --dictionary) and map lat/lon columns are excluded. This automatically turns on --smarter, and when --dictionary isn't already set, also turns on --dictionary infer (so the PII/identifier exclusion has semantic signal to work with). Capped at 50 columns (1,225 pairs); wider datasets skip these panels with a warning (run `qsv moarstats --bivariate` directly for the full pairwise output). Only affects `smart`. |  |
| &nbsp;`‑‑log‑scale`&nbsp; | string | Use a logarithmic y-axis for panels with a high dynamic range: frequency bar panels whose tallest bar dwarfs the rest (e.g. a large "(NULL)" or "Other (N)" bucket), so the small categories stay visible; and box panels of an all-positive column whose observed max/min ratio is huge (a box otherwise squashed against its extremes). One of: auto, on, off. "auto" (the default) switches a panel to a log y-axis only when its dynamic range is high; "on" forces a log y-axis on every frequency panel and every all-positive box panel; "off" keeps the linear axes. Only affects `smart`. | `auto` |
| &nbsp;`‑‑violin`&nbsp; | string | Draw distribution panels as violins (a box plot wrapped in a KDE density silhouette — a strict superset of a plain box) instead of boxes. One of: auto, on, off. "auto" (the default) draws a violin for every continuous column EXCEPT those on a log value axis (the KDE is estimated in linear space, which a log axis would geometrically distort) and those with fewer than 20 distinct values or fewer than 50 non-null observations (too few for a meaningful KDE — the curve would be smoothing artifact) — all keep an honest box; "on" also violins those; "off" never draws violins. Violin KDEs are clamped to each column's observed range (Plotly spanmode "hard") so a bounded or discrete measure can't show density past a natural limit. A column with at most 10,000 non-null values gets an exact violin (with outlier points, like a raw box); up to 30,000 values (one-fifth of the smart point budget, so it scales with QSV_VIZ_MAX_POINTS) still draws an exact violin but drops the point overlay; a larger column draws its silhouette and inner box from a deterministic sample of at most that many evenly strided values, carries no point overlay (a sample misses the true extremes), and is titled "(sampled)". An explicit all, outliers or suspected mode given via the box-points option instead collects every value for every panel, violins included; that option's `none` mode just hides a violin's points. Only affects `smart`. | `auto` |
| &nbsp;`‑‑title`&nbsp; | string | Chart title. |  |
| &nbsp;`‑‑x‑title`&nbsp; | string | X-axis title. (defaults to the x column name) |  |
| &nbsp;`‑‑y‑title`&nbsp; | string | Y-axis title. (defaults to the y column name) |  |
| &nbsp;`‑‑y‑range`&nbsp; | string | Fix the y-axis to an explicit min:max range (two colon- separated numbers, e.g. -10:55) instead of autoscaling; points outside are clipped from view. Handy when one extreme outlier squashes the rest. Applies to cartesian charts (bar/line/scatter/histogram/box/violin/heatmap/ contour). Pass a negative min with `=`: --y-range=-10:55. |  |
| &nbsp;`‑‑rangeslider`&nbsp; | flag | Add a draggable range-slider (a navigator strip) under the x-axis for panning and zooming. Best for an ordered x-axis (time series, line, candlestick/ohlc). Applies to cartesian charts (bar/line/scatter/histogram/box/violin/candlestick/ ohlc); off by default. |  |
| &nbsp;`‑‑slider`&nbsp; | string | Animate the chart over a column: each distinct value of the given column becomes an animation frame, with a Play/Pause button and a slider to scrub through the frames (Gapminder- style). Supported for bar/line/scatter and geo, and may be split into animated traces with the --series option. (Not supported for `viz map` — the MapLibre basemap can't animate; use `viz geo` for an animated point map.) On `viz scatter`, combining a slider with --series AND --size builds a Gapminder BUBBLE ANIMATION: --series is the entity (one colored bubble each), --x/--y the measure pair and --size the third data variable (bubble area). Each entity x frame cell collapses to one bubble via --agg (mean by default); bubble sizes are scaled once across all frames so they stay comparable. A slider column that parses as dates is auto-bucketed to a readable number of frames; any other column frames on its distinct values. For `smart`, this controls whether one animated relationship-over-time panel is added: the numeric pair whose per-time-bucket centroid path bends the most (a trailing-window scatter colored by time bucket), or an animated geo map of global-extent dated points, or a Gapminder entity-bubble chart (at most one is shown; when several qualify the bubble wins, then the geo map, then the pair). auto (default) adds one only on a strong signal (a date column and enough time buckets); on lowers the bar (as few as two buckets) but still requires a genuinely drifting or animatable relationship; off never does. Axis ranges are pinned across frames so nothing jumps. |  |
| &nbsp;`‑‑slider‑speed`&nbsp; | integer | Milliseconds each animation frame is shown while playing. | `800` |
| &nbsp;`‑‑slider‑cumulative`&nbsp; | flag | Accumulate rows across frames: frame N includes every row whose --slider value is at or below the Nth value, so the animation builds up cumulatively instead of replacing each step. Applies to bar/line/scatter and geo. On a bubble animation there is only ever one bubble per entity per frame, so nothing piles up on screen; instead each bubble shows a RUNNING aggregate over all frames up to that point, and so drifts less and less as the animation plays. |  |
| &nbsp;`‑‑annotation`&nbsp; | string | Caption note drawn at the bottom of the plot (e.g. to note a clipped outlier). Cartesian charts only. |  |
| &nbsp;`‑‑theme`&nbsp; | string | Plotly theme that drives the chart's overall look (background, fonts, axis styling). One of: default, plotly_white, plotly_dark, seaborn, seaborn_whitegrid, seaborn_dark, matplotlib, plotnine (case-insensitive; hyphens accepted). When omitted, qsv's built-in look is used. Applies to all chart types, including `smart`. |  |
| &nbsp;`‑‑language`&nbsp; | string | Render the Data Schematic UI in this language. Accepts a BCP-47/ISO 639 code (es, spa) or an English language name (Spanish). Overrides the language detected in a data dictionary, and is also forwarded to describegpt when inferring one. Curated: en, es, fr, de, it, pt-BR, ja, zh-CN. | `auto` |
| &nbsp;`‑‑width`&nbsp; | integer | Image width in pixels for static export. Default 1000; for `smart`, auto-scaled to the grid's column count. |  |
| &nbsp;`‑‑height`&nbsp; | integer | Image height in pixels for static export. Default 600; for `smart`, auto-scaled to the number of panel rows. |  |
| &nbsp;`‑‑scale`&nbsp; | float | Image scale factor (static export). | `1.0` |
| &nbsp;`‑‑open`&nbsp; | flag | Open the generated chart in the default browser/viewer. |  |

<a name="common-options"></a>

## Common Options [↩](#nav)

| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Option&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Type | Description | Default |
|--------|------|-------------|--------|
| &nbsp;`‑h,`<br>`‑‑help`&nbsp; | flag | Display this message |  |
| &nbsp;`‑o,`<br>`‑‑output`&nbsp; | string | Write output to <file> instead of stdout. The chart format is inferred from the extension: .html (default), .png, .svg, .pdf, .jpeg, .webp. |  |
| &nbsp;`‑d,`<br>`‑‑delimiter`&nbsp; | string | The field delimiter for reading CSV data. Must be a single character. (default: ,) |  |
| &nbsp;`‑n,`<br>`‑‑no‑headers`&nbsp; | flag | When set, the first row will not be interpreted as headers. Columns can then only be selected by index. |  |

---
**Source:** [`src/cmd/viz.rs`](https://github.com/dathere/qsv/blob/master/src/cmd/viz.rs)
| **[Table of Contents](TableOfContents.md)** | **[README](../../README.md)**
