Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pkg-py/src/querychat/_querychat_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,8 +830,8 @@ def normalize_tools(
return resolved
if resolved is not None and has_viz_tool(resolved) and not has_viz_deps():
warnings.warn(
"Visualization tools require ggsql, altair, shinywidgets, and "
"vl-convert-python. Install them with: pip install querychat[viz]. "
"Visualization tools require ggsql>=0.5.2, altair, shinywidgets, "
"and vl-convert-python. Install them with: pip install querychat[viz]. "
"Continuing without the 'visualize' tool.",
stacklevel=2,
)
Expand Down
27 changes: 24 additions & 3 deletions pkg-py/src/querychat/_viz_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

from __future__ import annotations

import importlib.metadata
import importlib.util

from htmltools import HTMLDependency, tags

from .__version import __version__

MIN_GGSQL_VERSION = (0, 5, 2)


def has_viz_tool(tools: set[str] | None) -> bool:
"""Check if visualize is among the configured tools."""
Expand All @@ -16,10 +19,28 @@ def has_viz_tool(tools: set[str] | None) -> bool:

def has_viz_deps() -> bool:
"""Check whether visualization dependencies (ggsql, altair, shinywidgets, vl-convert-python) are installed."""
return all(
importlib.util.find_spec(pkg) is not None
if any(
importlib.util.find_spec(pkg) is None
for pkg in ("ggsql", "altair", "shinywidgets", "vl_convert")
)
):
return False
return ggsql_version() >= MIN_GGSQL_VERSION


def ggsql_version() -> tuple[int, ...]:
"""Installed ggsql version as a comparable tuple (e.g. ``(0, 5, 2)``)."""
raw = importlib.metadata.version("ggsql")
parts: list[int] = []
for piece in raw.split("."):
num = ""
for ch in piece:
if not ch.isdigit():
break
num += ch
if not num:
break
parts.append(int(num))
return tuple(parts)


PRELOAD_WIDGET_ID = "__querychat_preload_viz__"
Expand Down
27 changes: 21 additions & 6 deletions pkg-py/src/querychat/prompts/ggsql-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ VISUALISE date AS x, revenue AS y
DRAW line
```

`VISUALISE FROM` takes exactly one **bare** source — an alias (`FROM tbl AS t`), a join, or a comma-separated list of tables is a parse error. To combine tables, join them in the SELECT clause and visualise the result.

### Mapping Styles

| Style | Syntax | Use When |
Expand Down Expand Up @@ -79,9 +81,11 @@ DRAW geom_type
- `arrow` draws arrows between two points. Requires `x`, `y`, `xend`, `yend` aesthetics.
- `rule` draws full-span reference lines. Map a value to `y` for a horizontal line or `x` for a vertical line. Optionally map `slope` to create diagonal reference lines: `y = a + slope * x` (when `y` is mapped) or `x = a + slope * y` (when `x` is mapped).
- `rect` draws rectangles. Pick 2 per axis from center (`x`/`y`), min (`xmin`/`ymin`), max (`xmax`/`ymax`), `width`, `height`. Or just map center (defaults to width/height of 1).
- `range` displays interval marks. Requires `x`, `ymin`, `ymax` for vertical intervals, or `y`, `xmin`, `xmax` for horizontal intervals. Use it for confidence intervals, lollipops, and candlestick-style ranges. Setting `width => null` hides the hinges.
- `range` displays interval marks. Requires `x`, `ymin`, `ymax` for vertical intervals, or `y`, `xmin`, `xmax` for horizontal intervals. Use it for confidence intervals, lollipops, and candlestick-style ranges. The `hinge` setting controls the width of the endpoint caps in points (default 10); set `hinge => null` to hide the hinges.
- `segment` draws arbitrary connections between two points and requires `x`, `y`, `xend`, and `yend`.
- `line` and `path` support continuously varying `linewidth`, `stroke`, and `opacity` aesthetics within groups.
- Use `SETTING position => 'jitter'` on `point` layers to spread overlapping points along a discrete axis (strip-plot style). Jitter settings: `width` (0–1, default 0.4), `distribution` (`'uniform'` default, `'normal'`, `'density'`, `'intensity'`), and `side` (`'both'` default; a one-sided value pairs with a half-violin or half-boxplot on the same band).
- Identity-stat layers (`point`, `line`, `area`, `bar`, `ribbon`, `range`, `segment`, `arrow`, `rule`, `text`) accept an `aggregate` setting that collapses each group (defined by `PARTITION BY` plus all discrete mappings) to one row, replacing every numeric mapping with its aggregated value — summarising in the layer instead of writing a `GROUP BY`. Takes `'<func>'` (applies to every numeric mapping; e.g. `count`, `sum`, `min`, `max`, `mean`, `median`) or `'<aes>:<func>'` to target one aesthetic. Example: `VISUALISE species AS x, bill_length AS y FROM penguins` + `DRAW bar SETTING aggregate => 'mean'` draws the mean bill length per species.

**Aesthetics (MAPPING):**

Expand Down Expand Up @@ -147,14 +151,16 @@ Some layers compute statistics. Use REMAPPING to access computed values:
| `smooth` | `intensity` | `intensity AS y` |
| `boxplot` | `value`, `type` | `value AS y` |

`boxplot` displays box-and-whisker plots. Settings: `outliers` (`true` default — show outlier points), `coef` (`1.5` default — whisker fence coefficient), `width` (`0.9` default — box width, 0–1).
`boxplot` displays box-and-whisker plots. Settings: `outliers` (`true` default — show outlier points), `coef` (`1.5` default — whisker fence coefficient), `width` (`0.9` default — box width, 0–1), `hinge` (whisker-cap width in points, default `null` = no caps), `side` (`'both'` default; `'left'`/`'bottom'` or `'right'`/`'top'` for a half-boxplot that pairs with a half-violin or one-sided jitter on the same band).

`smooth` fits a trendline to data. Settings: `method` (`'nw'` or `'nadaraya-watson'` default kernel regression, `'ols'` for OLS linear, `'tls'` for total least squares). NW-only settings: `bandwidth` (numeric), `adjust` (multiplier, default 1), `kernel` (`'gaussian'` default, `'epanechnikov'`, `'triangular'`, `'rectangular'`, `'uniform'`, `'biweight'`, `'quartic'`, `'cosine'`).

`density` computes a KDE from a continuous `x`. Settings: `bandwidth` (numeric), `adjust` (multiplier, default 1), `kernel` (`'gaussian'` default, `'epanechnikov'`, `'triangular'`, `'rectangular'`, `'uniform'`, `'biweight'`, `'quartic'`, `'cosine'`). Use `REMAPPING intensity AS y` to show unnormalized density that reflects group size differences. Use `SETTING position => 'stack'` for stacked densities.

`violin` displays mirrored KDE curves for groups. Requires both `x` (categorical) and `y` (continuous). Accepts the same bandwidth/adjust/kernel settings as density. Use `REMAPPING intensity AS offset` to reflect group size differences. Additional settings: `side` (`'both'` default, `'left'`/`'bottom'`, `'right'`/`'top'` — for half-violin/ridgeline plots), `width` (any value > 0; values > 1 enable ridgeline-style overlapping).

`boxplot`, `violin`, and `range` may omit the categorical aesthetic (e.g. `DRAW boxplot MAPPING salary AS y`) to summarise all rows as a single mark.

**Examples:**

```sql
Expand Down Expand Up @@ -297,6 +303,7 @@ PROJECT TO polar SETTING start => -90, end => 90 -- half-circle gauge
- `start` — starting angle in degrees (0 = 12 o'clock, 90 = 3 o'clock)
- `end` — ending angle in degrees (default: start + 360; use for partial arcs/gauges)
- `inner` — inner radius as proportion 0–1 (0 = full pie, 0.5 = donut with 50% hole)
- `radar` — `true` displays discrete-angle data as a radar plot
- `clip` — clip out-of-bounds data (default `true`)

**Axis flipping:** To create horizontal bar charts or flip axes, use `PROJECT y, x TO cartesian`. This maps anything on `y` to the horizontal axis and `x` to the vertical axis.
Expand Down Expand Up @@ -328,14 +335,14 @@ SCALE panel FROM ('Biscoe', 'Dream')

### LABEL Clause

Use LABEL for axis labels, subtitles, and captions. Do NOT use `LABEL title => ...` — the tool's `title` parameter handles chart titles. Set a label to `null` to suppress it.
Use LABEL for axis labels and subtitles. Do NOT use `LABEL title => ...` — the tool's `title` parameter handles chart titles. Set a label to `null` to suppress it.

Available labels: any aesthetic name (`x`, `y`, `fill`, `color`, etc.), `subtitle`, `caption`.
Available labels: any aesthetic name (`x`, `y`, `fill`, `color`, etc.) and `subtitle`. (ggsql also has a `caption` label, but it is ignored under Vega-Lite rendering — see below.)

```sql
LABEL x => 'X Axis Label', y => 'Y Axis Label'
LABEL x => null -- suppress x-axis label
LABEL subtitle => 'Q4 2024 data', caption => 'Source: internal database'
LABEL subtitle => 'Q4 2024 data — Source: internal database'
```

## Complete Examples
Expand Down Expand Up @@ -452,7 +459,7 @@ DRAW text MAPPING n AS label SETTING offset => (0, -11), fill => 'white'
```sql
SELECT ROUND(bill_dep) AS bill_dep, COUNT(*) AS n FROM penguins GROUP BY 1
VISUALISE bill_dep AS x, n AS y
DRAW range MAPPING 0 AS ymin, n AS ymax SETTING width => null
DRAW range MAPPING 0 AS ymin, n AS ymax SETTING hinge => null
DRAW point
```

Expand Down Expand Up @@ -551,3 +558,11 @@ PROJECT TO polar SETTING inner => 0.5
DRAW bar MAPPING category AS x, subcategory AS fill SETTING position => 'dodge' -- side-by-side
DRAW bar MAPPING category AS x, subcategory AS fill SETTING position => 'stack', total => 1 -- proportional
```

## Vega-Lite rendering

querychat draws all charts with ggsql's **Vega-Lite** writer, not its native SVG/PDF writers. Writer-dependent behavior to know:

- **Label text renders literally**: markdown such as `**bold**` or `` `code` `` would appear verbatim in the chart — keep label text plain.
- **The `minor_breaks` scale setting is ignored** — no minor gridlines are drawn.
- **`LABEL caption` is ignored** — captions have no Vega-Lite equivalent and never appear in the chart. Put source attributions or footnotes in the `subtitle` instead.
4 changes: 2 additions & 2 deletions pkg-r/DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Suggests:
dplyr,
DT,
duckdb,
ggsql,
ggsql (>= 0.5.2),
knitr,
later,
nanoparquet,
Expand All @@ -61,7 +61,7 @@ Suggests:
withr
VignetteBuilder:
knitr
Config/roxygen2/version: 8.1.0
Config/roxygen2/version: 8.1.1
Config/testthat/edition: 3
Config/testthat/parallel: true
Encoding: UTF-8
Expand Down
6 changes: 4 additions & 2 deletions pkg-r/R/QueryChat.R
Original file line number Diff line number Diff line change
Expand Up @@ -1652,13 +1652,15 @@ normalize_tools <- function(tools) {

check_viz_deps <- function(tools) {
if (
is.null(tools) || !"visualize" %in% tools || rlang::is_installed("ggsql")
is.null(tools) ||
!"visualize" %in% tools ||
rlang::is_installed("ggsql", version = "0.5.2")
) {
return(tools)
}
rlang::warn(
c(
'The "visualize" tool requires the {.pkg ggsql} package.',
'The "visualize" tool requires {.pkg ggsql} 0.5.2 or later.',
"i" = 'Install it with `install.packages("ggsql")`; continuing without the "visualize" tool.'
),
.frequency = "once",
Expand Down
27 changes: 21 additions & 6 deletions pkg-r/inst/prompts/ggsql-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ VISUALISE date AS x, revenue AS y
DRAW line
```

`VISUALISE FROM` takes exactly one **bare** source — an alias (`FROM tbl AS t`), a join, or a comma-separated list of tables is a parse error. To combine tables, join them in the SELECT clause and visualise the result.

### Mapping Styles

| Style | Syntax | Use When |
Expand Down Expand Up @@ -79,9 +81,11 @@ DRAW geom_type
- `arrow` draws arrows between two points. Requires `x`, `y`, `xend`, `yend` aesthetics.
- `rule` draws full-span reference lines. Map a value to `y` for a horizontal line or `x` for a vertical line. Optionally map `slope` to create diagonal reference lines: `y = a + slope * x` (when `y` is mapped) or `x = a + slope * y` (when `x` is mapped).
- `rect` draws rectangles. Pick 2 per axis from center (`x`/`y`), min (`xmin`/`ymin`), max (`xmax`/`ymax`), `width`, `height`. Or just map center (defaults to width/height of 1).
- `range` displays interval marks. Requires `x`, `ymin`, `ymax` for vertical intervals, or `y`, `xmin`, `xmax` for horizontal intervals. Use it for confidence intervals, lollipops, and candlestick-style ranges. Setting `width => null` hides the hinges.
- `range` displays interval marks. Requires `x`, `ymin`, `ymax` for vertical intervals, or `y`, `xmin`, `xmax` for horizontal intervals. Use it for confidence intervals, lollipops, and candlestick-style ranges. The `hinge` setting controls the width of the endpoint caps in points (default 10); set `hinge => null` to hide the hinges.
- `segment` draws arbitrary connections between two points and requires `x`, `y`, `xend`, and `yend`.
- `line` and `path` support continuously varying `linewidth`, `stroke`, and `opacity` aesthetics within groups.
- Use `SETTING position => 'jitter'` on `point` layers to spread overlapping points along a discrete axis (strip-plot style). Jitter settings: `width` (0–1, default 0.4), `distribution` (`'uniform'` default, `'normal'`, `'density'`, `'intensity'`), and `side` (`'both'` default; a one-sided value pairs with a half-violin or half-boxplot on the same band).
- Identity-stat layers (`point`, `line`, `area`, `bar`, `ribbon`, `range`, `segment`, `arrow`, `rule`, `text`) accept an `aggregate` setting that collapses each group (defined by `PARTITION BY` plus all discrete mappings) to one row, replacing every numeric mapping with its aggregated value — summarising in the layer instead of writing a `GROUP BY`. Takes `'<func>'` (applies to every numeric mapping; e.g. `count`, `sum`, `min`, `max`, `mean`, `median`) or `'<aes>:<func>'` to target one aesthetic. Example: `VISUALISE species AS x, bill_length AS y FROM penguins` + `DRAW bar SETTING aggregate => 'mean'` draws the mean bill length per species.

**Aesthetics (MAPPING):**

Expand Down Expand Up @@ -147,14 +151,16 @@ Some layers compute statistics. Use REMAPPING to access computed values:
| `smooth` | `intensity` | `intensity AS y` |
| `boxplot` | `value`, `type` | `value AS y` |

`boxplot` displays box-and-whisker plots. Settings: `outliers` (`true` default — show outlier points), `coef` (`1.5` default — whisker fence coefficient), `width` (`0.9` default — box width, 0–1).
`boxplot` displays box-and-whisker plots. Settings: `outliers` (`true` default — show outlier points), `coef` (`1.5` default — whisker fence coefficient), `width` (`0.9` default — box width, 0–1), `hinge` (whisker-cap width in points, default `null` = no caps), `side` (`'both'` default; `'left'`/`'bottom'` or `'right'`/`'top'` for a half-boxplot that pairs with a half-violin or one-sided jitter on the same band).

`smooth` fits a trendline to data. Settings: `method` (`'nw'` or `'nadaraya-watson'` default kernel regression, `'ols'` for OLS linear, `'tls'` for total least squares). NW-only settings: `bandwidth` (numeric), `adjust` (multiplier, default 1), `kernel` (`'gaussian'` default, `'epanechnikov'`, `'triangular'`, `'rectangular'`, `'uniform'`, `'biweight'`, `'quartic'`, `'cosine'`).

`density` computes a KDE from a continuous `x`. Settings: `bandwidth` (numeric), `adjust` (multiplier, default 1), `kernel` (`'gaussian'` default, `'epanechnikov'`, `'triangular'`, `'rectangular'`, `'uniform'`, `'biweight'`, `'quartic'`, `'cosine'`). Use `REMAPPING intensity AS y` to show unnormalized density that reflects group size differences. Use `SETTING position => 'stack'` for stacked densities.

`violin` displays mirrored KDE curves for groups. Requires both `x` (categorical) and `y` (continuous). Accepts the same bandwidth/adjust/kernel settings as density. Use `REMAPPING intensity AS offset` to reflect group size differences. Additional settings: `side` (`'both'` default, `'left'`/`'bottom'`, `'right'`/`'top'` — for half-violin/ridgeline plots), `width` (any value > 0; values > 1 enable ridgeline-style overlapping).

`boxplot`, `violin`, and `range` may omit the categorical aesthetic (e.g. `DRAW boxplot MAPPING salary AS y`) to summarise all rows as a single mark.

**Examples:**

```sql
Expand Down Expand Up @@ -297,6 +303,7 @@ PROJECT TO polar SETTING start => -90, end => 90 -- half-circle gauge
- `start` — starting angle in degrees (0 = 12 o'clock, 90 = 3 o'clock)
- `end` — ending angle in degrees (default: start + 360; use for partial arcs/gauges)
- `inner` — inner radius as proportion 0–1 (0 = full pie, 0.5 = donut with 50% hole)
- `radar` — `true` displays discrete-angle data as a radar plot
- `clip` — clip out-of-bounds data (default `true`)

**Axis flipping:** To create horizontal bar charts or flip axes, use `PROJECT y, x TO cartesian`. This maps anything on `y` to the horizontal axis and `x` to the vertical axis.
Expand Down Expand Up @@ -328,14 +335,14 @@ SCALE panel FROM ('Biscoe', 'Dream')

### LABEL Clause

Use LABEL for axis labels, subtitles, and captions. Do NOT use `LABEL title => ...` — the tool's `title` parameter handles chart titles. Set a label to `null` to suppress it.
Use LABEL for axis labels and subtitles. Do NOT use `LABEL title => ...` — the tool's `title` parameter handles chart titles. Set a label to `null` to suppress it.

Available labels: any aesthetic name (`x`, `y`, `fill`, `color`, etc.), `subtitle`, `caption`.
Available labels: any aesthetic name (`x`, `y`, `fill`, `color`, etc.) and `subtitle`. (ggsql also has a `caption` label, but it is ignored under Vega-Lite rendering — see below.)

```sql
LABEL x => 'X Axis Label', y => 'Y Axis Label'
LABEL x => null -- suppress x-axis label
LABEL subtitle => 'Q4 2024 data', caption => 'Source: internal database'
LABEL subtitle => 'Q4 2024 data — Source: internal database'
```

## Complete Examples
Expand Down Expand Up @@ -452,7 +459,7 @@ DRAW text MAPPING n AS label SETTING offset => (0, -11), fill => 'white'
```sql
SELECT ROUND(bill_dep) AS bill_dep, COUNT(*) AS n FROM penguins GROUP BY 1
VISUALISE bill_dep AS x, n AS y
DRAW range MAPPING 0 AS ymin, n AS ymax SETTING width => null
DRAW range MAPPING 0 AS ymin, n AS ymax SETTING hinge => null
DRAW point
```

Expand Down Expand Up @@ -551,3 +558,11 @@ PROJECT TO polar SETTING inner => 0.5
DRAW bar MAPPING category AS x, subcategory AS fill SETTING position => 'dodge' -- side-by-side
DRAW bar MAPPING category AS x, subcategory AS fill SETTING position => 'stack', total => 1 -- proportional
```

## Vega-Lite rendering

querychat draws all charts with ggsql's **Vega-Lite** writer, not its native SVG/PDF writers. Writer-dependent behavior to know:

- **Label text renders literally**: markdown such as `**bold**` or `` `code` `` would appear verbatim in the chart — keep label text plain.
- **The `minor_breaks` scale setting is ignored** — no minor gridlines are drawn.
- **`LABEL caption` is ignored** — captions have no Vega-Lite equivalent and never appear in the chart. Put source attributions or footnotes in the `subtitle` instead.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ gradio = ["gradio>=6.0"]
dash = ["dash-ag-grid>=31.0", "dash[async]>=3.1", "dash-bootstrap-components>=2.0", "pandas"]
# Visualization with ggsql
viz = [
"ggsql>=0.3.2",
"ggsql>=0.5.2",
"altair>=6.0",
"shinywidgets>=0.8.0",
"vl-convert-python>=1.9.0",
Expand Down Expand Up @@ -117,7 +117,7 @@ dev = [
"ibis-framework[duckdb]>=9.0.0",
# see note on the `ibis` extra above
"sqlglot<30.18.0",
"ggsql>=0.3.2",
"ggsql>=0.5.2",
"altair>=6.0",
"shinywidgets>=0.8.0",
"vl-convert-python>=1.9.0",
Expand Down
Loading