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
7 changes: 3 additions & 4 deletions doc/syntax/layer/type/area.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,9 @@ We can reshape the data to 'long format' from our wide format.

```{ggsql}
CREATE TABLE long_airquality AS
SELECT * FROM ggsql:airquality
UNPIVOT(
Value FOR Series IN (Temp, Wind)
) AS u;
SELECT Date, 'Temp' AS Series, Temp AS Value FROM ggsql:airquality
UNION ALL
SELECT Date, 'Wind' AS Series, Wind AS Value FROM ggsql:airquality;
```

Which means we can display multiple series at once, by mapping the identifier to an aesthetic.
Expand Down
14 changes: 7 additions & 7 deletions doc/syntax/layer/type/errorbar.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ The orientation of errorbar layers is deduced directly from the mapping, because
#| code-fold: true
#| code-summary: "Create example data"
CREATE TABLE penguin_summary AS
SELECT
species,
MEAN(bill_dep) - STDDEV(bill_dep) AS low,
MEAN(bill_dep) AS mean,
MEAN(bill_dep) + STDDEV(bill_dep) AS high
FROM ggsql:penguins
GROUP BY species
WITH stats AS (
SELECT species, AVG(bill_dep) AS mean
FROM ggsql:penguins
GROUP BY species
)
SELECT species, mean - 1.5 AS low, mean, mean + 1.5 AS high
FROM stats
```

Classic errorbar with point at centre.
Expand Down
10 changes: 4 additions & 6 deletions doc/syntax/layer/type/linear.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,10 @@ VISUALISE FROM ggsql:penguins
Add multiple reference lines with different colors from a separate dataset. Note we're mapping from data here, so we use `DRAW` instead of `PLACE`.

```{ggsql}
WITH lines AS (
SELECT * FROM (VALUES
(0.4, -1, 'Line A'),
(0.2, 8, 'Line B'),
(0.8, -19, 'Line C')
) AS t(coef, intercept, label)
WITH lines(coef, intercept, label) AS (VALUES
(0.4, -1, 'Line A'),
(0.2, 8, 'Line B'),
(0.8, -19, 'Line C')
)
VISUALISE FROM ggsql:penguins
DRAW point MAPPING bill_len AS x, bill_dep AS y
Expand Down
7 changes: 4 additions & 3 deletions doc/syntax/layer/type/path.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ The path layer has no orientation. The axes are treated symmetrically.
#| code-fold: true
#| code-summary: "Create example data"
CREATE TABLE df AS
SELECT * FROM (VALUES
WITH t(x, y, id) AS (VALUES
(1.0, 1.0, 'A'),
(2.0, 1.0, 'A'),
(1.0, 3.0, 'A'),
(3.0, 1.0, 'B'),
(2.0, 3.0, 'B'),
(3.0, 3.0, 'B'),
) AS t(x, y, id)
(3.0, 3.0, 'B')
)
SELECT * FROM t
```

Simple example path.
Expand Down
7 changes: 4 additions & 3 deletions doc/syntax/layer/type/polygon.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ The polygon layer has no orientation. The axes are treated symmetrically.
#| code-fold: true
#| code-summary: "Create example data"
CREATE TABLE df AS
SELECT * FROM (VALUES
WITH t(x, y, id) AS (VALUES
(1.0, 1.0, 'A'),
(1.0, 3.0, 'A'),
(2.0, 1.0, 'A'),
(2.0, 3.0, 'B'),
(3.0, 1.0, 'B'),
(3.0, 3.0, 'B'),
) AS t(x, y, id)
(3.0, 3.0, 'B')
)
SELECT * FROM t
```

Simple example polygon.
Expand Down
7 changes: 3 additions & 4 deletions doc/syntax/layer/type/rect.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Rectangle"
---

> Layers are declared with the [`DRAW` clause](../clause/draw.qmd). Read the documentation for this clause for a thorough description of how to use it.
> Layers are declared with the [`DRAW` clause](../../clause/draw.qmd). Read the documentation for this clause for a thorough description of how to use it.

Rectangles can be used to draw heatmaps or indicate ranges.

Expand Down Expand Up @@ -85,8 +85,7 @@ SELECT
MIN(Temp) AS min,
MAX(Temp) AS max
FROM ggsql:airquality
GROUP BY
WEEKOFYEAR(Date)
GROUP BY Week

VISUALISE start AS xmin, end AS xmax, min AS ymin, max AS ymax
DRAW rect
Expand All @@ -103,4 +102,4 @@ VISUALISE FROM ggsql:airquality
ymax => 100,
colour => 'dodgerblue'
DRAW line MAPPING Date AS x, Temp AS y
```
```
12 changes: 6 additions & 6 deletions doc/syntax/layer/type/ribbon.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ Ribbon plots are great for showing the range of some aggregation.

```{ggsql}
// Weekly aggregation of temperature
SELECT
WEEKOFYEAR(Date) AS Week,
MAX(Temp) AS MaxTemp,
MEAN(Temp) AS MeanTemp,
MIN(Temp) AS MinTemp
SELECT
Week,
MAX(Temp) AS MaxTemp,
AVG(Temp) AS MeanTemp,
MIN(Temp) AS MinTemp
FROM ggsql:airquality
GROUP BY WEEKOFYEAR(Date)
GROUP BY Week

VISUALISE Week AS x
DRAW ribbon
Expand Down
10 changes: 4 additions & 6 deletions doc/syntax/layer/type/rule.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ VISUALISE FROM ggsql:penguins
Add multiple threshold lines with different colors. Note that because we're mapping data from data, we use the `DRAW` clause instead of the `PLACE` clause.

```{ggsql}
WITH thresholds AS (
SELECT * FROM (VALUES
(70, 'Target'),
(80, 'Warning'),
(90, 'Critical')
) AS t(value, label)
WITH thresholds(value, label) AS (VALUES
(70, 'Target'),
(80, 'Warning'),
(90, 'Critical')
)
SELECT Date AS date, temp AS temperature
FROM ggsql:airquality
Expand Down
22 changes: 10 additions & 12 deletions doc/syntax/layer/type/segment.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,15 @@ The segment layer has no orientations. The axes are treated symmetrically.
Segments are useful when you have known start and end points of the data. For example in a graph

```{ggsql}
WITH edges AS (
SELECT * FROM (VALUES
(0, 0, 1, 1, 'A'),
(1, 1, 2, 1, 'A'),
(2, 1, 3, 0, 'A'),
(0, 3, 1, 2, 'B'),
(1, 2, 2, 2, 'B'),
(2, 2, 3, 3, 'B'),
(1, 1, 1, 2, 'C'),
(2, 2, 2, 1, 'C')
) AS t(x, y, xend, yend, type)
WITH edges(x, y, xend, yend, type) AS (VALUES
(0, 0, 1, 1, 'A'),
(1, 1, 2, 1, 'A'),
(2, 1, 3, 0, 'A'),
(0, 3, 1, 2, 'B'),
(1, 2, 2, 2, 'B'),
(2, 2, 3, 3, 'B'),
(1, 1, 1, 2, 'C'),
(2, 2, 2, 1, 'C')
)
VISUALISE x, y, xend, yend FROM edges
DRAW segment MAPPING type AS stroke
Expand Down Expand Up @@ -81,7 +79,7 @@ SELECT
ELSE 'warmer'
END AS trend
FROM ggsql:airquality
GROUP BY WEEKOFYEAR(Date)
GROUP BY Week

VISUALISE date AS x, trend AS colour
DRAW segment
Expand Down
12 changes: 6 additions & 6 deletions doc/syntax/layer/type/smooth.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Smooth"
---

> Layers are declared with the [`DRAW` clause](../clause/draw.qmd). Read the documentation for this clause for a thorough description of how to use it.
> Layers are declared with the [`DRAW` clause](../../clause/draw.qmd). Read the documentation for this clause for a thorough description of how to use it.

Smooth layers are used to display a trendline among a series of observations.

Expand Down Expand Up @@ -119,17 +119,17 @@ The default `method => 'nw'` might be too coarse for timeseries.
<!-- Ideally, we would just use the date here directly but we currently require numeric data -->

```{ggsql}
SELECT *, EPOCH(Date) AS numdate FROM ggsql:airquality
VISUALISE numdate AS x, Temp AS y
SELECT * FROM ggsql:airquality
VISUALISE Epoch AS x, Temp AS y
DRAW point
DRAW smooth
```

You can make the fit more granular by reducing the bandwidth, for example using `adjust`.

```{ggsql}
SELECT *, EPOCH(Date) AS numdate FROM ggsql:airquality
VISUALISE numdate AS x, Temp AS y
SELECT * FROM ggsql:airquality
VISUALISE Epoch AS x, Temp AS y
DRAW point
DRAW smooth SETTING adjust => 0.2
```
Expand All @@ -150,4 +150,4 @@ VISUALISE bill_len AS x, bill_dep AS y, species AS stroke FROM ggsql:penguins
DRAW point SETTING opacity => 0
DRAW smooth SETTING method => 'ols'
DRAW smooth MAPPING 'All' AS stroke SETTING method => 'ols'
```
```
11 changes: 6 additions & 5 deletions doc/syntax/scale/type/identity.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ SCALE IDENTITY size
#### Use color values directly

```{ggsql}
SELECT * FROM (VALUES
('A', 45, 'forestgreen'),
('B', 72, '#3401e3'),
('C', 38, 'hsl(150deg 30% 60%)')
) AS t(category, value, style)
WITH t(category, value, style) AS (VALUES
('A', 45, 'forestgreen'),
('B', 72, '#3401e3'),
('C', 38, 'hsl(150deg 30% 60%)')
)
SELECT * FROM t
VISUALISE category AS x, value AS y, style AS fill
DRAW bar
SCALE IDENTITY fill
Expand Down
54 changes: 49 additions & 5 deletions ggsql-jupyter/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,32 @@ use serde_json::{json, Value};

/// Format execution result as Jupyter display_data content
///
/// Returns a JSON value matching the Jupyter display_data message format:
/// Returns `Some(Value)` for results that should be displayed, or `None` for
/// empty results (e.g., DDL statements like CREATE TABLE that have no columns).
///
/// Note: A SELECT that returns 0 rows but has columns will still display
/// an empty table with headers. Only truly empty DataFrames (0 columns)
/// from DDL statements return `None`.
///
/// The returned JSON matches the Jupyter display_data message format:
/// ```json
/// {
/// "data": { "mime/type": content, ... },
/// "metadata": { ... },
/// "transient": { ... }
/// }
/// ```
pub fn format_display_data(result: ExecutionResult) -> Value {
pub fn format_display_data(result: ExecutionResult) -> Option<Value> {
match result {
ExecutionResult::Visualization { spec } => format_vegalite(spec),
ExecutionResult::DataFrame(df) => format_dataframe(df),
ExecutionResult::Visualization { spec } => Some(format_vegalite(spec)),
ExecutionResult::DataFrame(df) => {
// DDL statements return DataFrames with 0 columns - don't display anything
if df.width() == 0 {
None
} else {
Some(format_dataframe(df))
}
}
}
}

Expand Down Expand Up @@ -199,12 +213,42 @@ mod tests {
fn test_vegalite_format() {
let spec = r#"{"mark": "point"}"#.to_string();
let result = ExecutionResult::Visualization { spec };
let display = format_display_data(result);
let display = format_display_data(result).expect("Visualization should return Some");

assert!(display["data"]["text/html"].is_string());
assert!(display["data"]["text/plain"].is_string());
}

#[test]
fn test_empty_dataframe_returns_none() {
use polars::prelude::*;

// DDL statements return DataFrames with 0 columns
let df = DataFrame::new(Vec::<Column>::new()).unwrap();
let result = ExecutionResult::DataFrame(df);
let display = format_display_data(result);

assert!(
display.is_none(),
"Empty DataFrame (0 columns) should return None"
);
}

#[test]
fn test_empty_rows_dataframe_returns_some() {
use polars::prelude::*;

// SELECT with 0 rows but columns should still display
let df = DataFrame::new(vec![Column::new("x".into(), Vec::<i32>::new())]).unwrap();
let result = ExecutionResult::DataFrame(df);
let display = format_display_data(result);

assert!(
display.is_some(),
"DataFrame with columns but 0 rows should return Some"
);
}

#[test]
fn test_html_escape() {
assert_eq!(
Expand Down
29 changes: 15 additions & 14 deletions ggsql-jupyter/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,23 +312,24 @@ impl KernelServer {
Ok(exec_result) => {
// Send execute_result (not display_data)
// Per Jupyter spec: execute_result includes execution_count
// Only send if there's something to display (DDL returns None)
if !silent {
let display_data = format_display_data(exec_result);
if let Some(display_data) = format_display_data(exec_result) {
// Build message content, including output_location if present
let mut content = json!({
"execution_count": self.execution_count,
"data": display_data["data"],
"metadata": display_data["metadata"]
});

// Add output_location for Positron routing (e.g., to Plots pane)
if let Some(location) = display_data.get("output_location") {
content["output_location"] = location.clone();
tracing::info!("Setting output_location: {}", location);
}

// Build message content, including output_location if present
let mut content = json!({
"execution_count": self.execution_count,
"data": display_data["data"],
"metadata": display_data["metadata"]
});

// Add output_location for Positron routing (e.g., to Plots pane)
if let Some(location) = display_data.get("output_location") {
content["output_location"] = location.clone();
tracing::info!("Setting output_location: {}", location);
self.send_iopub("execute_result", content, parent).await?;
}

self.send_iopub("execute_result", content, parent).await?;
}

// Send execute_reply
Expand Down
Binary file modified src/data/airquality.parquet
Binary file not shown.
Loading