Skip to content
4 changes: 4 additions & 0 deletions doc/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ website:
href: syntax/clause/visualise.qmd
- text: "`DRAW`"
href: syntax/clause/draw.qmd
- text: "`PLACE`"
href: syntax/clause/place.qmd
- text: "`SCALE`"
href: syntax/clause/scale.qmd
- text: "`FACET`"
Expand Down Expand Up @@ -69,6 +71,8 @@ website:
href: syntax/clause/visualise.qmd
- text: "`DRAW`"
href: syntax/clause/draw.qmd
- text: "`PLACE`"
href: syntax/clause/place.qmd
- text: "`SCALE`"
href: syntax/clause/scale.qmd
- text: "`FACET`"
Expand Down
37 changes: 37 additions & 0 deletions doc/syntax/clause/place.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "Create annotation layers with `PLACE`"
---

The `PLACE` clause is the little sibling of the [`DRAW` clause](../clause/draw.qmd) that also creates layers.
A layer created with `PLACE` has no mappings to data, all aesthetics are set using literal values instead.

## Clause syntax
The `PLACE` clause takes just a type and a setting clause, all of them required.

```ggsql
PLACE <layer-type>
SETTING <parameter/aesthetic> => <value>, ...
```

Like `DRAW`, the layer type is required and specifies the type of layer to draw, like `point` or `text`.
It defines how the remaining settings are interpreted.
The [main syntax page](../index.qmd#layers) has a list of all available layer types

Unlike the `DRAW` clause, the `PLACE` clause does not support `FILTER`, `PARTITION BY`, and `ORDER BY` clauses since
everything is declared inline.

### `SETTING`
```ggsql
SETTING <parameter/aesthetic> => <value>, ...
```

The `SETTING` clause can be used for to different things:

* *Setting aesthetics*: All aesthetics in `PLACE` layers are specified using literal value, e.g. 'red' (as in the color red).
Aesthetics that are set will not go through a scale but will use the provided value as-is.
You cannot set an aesthetic to a column, only to a literal values.
Contrary to `DRAW` layers, `PLACE` layers can take multiple literal values in an array.
* *Setting parameters*: Some layers take additional arguments that control how they behave.
Often, but not always, these modify the statistical transformation in some way.
An example would be the binwidth parameter in histogram which controls the width of each bin during histogram calculation.
This is not a statistical property since it is not related to each record, but to the calculation as a whole.
1 change: 1 addition & 0 deletions doc/syntax/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ggsql augments the standard SQL syntax with a number of new clauses to describe

- [`VISUALISE`](clause/visualise.qmd) initiates the visualisation part of the query
- [`DRAW`](clause/draw.qmd) adds a new layer to the visualisation
- [`PLACE`](clause/place.qmd) adds an annotation layer
- [`SCALE`](clause/scale.qmd) specify how an aesthetic should be scaled
- [`FACET`](clause/facet.qmd) describes how data should be split into small multiples
- [`PROJECT`](clause/project.qmd) is used for selecting the coordinate system to use
Expand Down
37 changes: 18 additions & 19 deletions src/execute/casting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! scale requirements and updating type info accordingly.

use crate::plot::scale::coerce_dtypes;
use crate::plot::{CastTargetType, Layer, ParameterValue, Plot, SqlTypeNames};
use crate::plot::{CastTargetType, Layer, Plot, SqlTypeNames};
use crate::{naming, DataSource};
use polars::prelude::{DataType, TimeUnit};
use std::collections::{HashMap, HashSet};
Expand All @@ -22,24 +22,6 @@ pub struct TypeRequirement {
pub sql_type_name: String,
}

/// Format a literal value as SQL
pub fn literal_to_sql(lit: &ParameterValue) -> String {
match lit {
ParameterValue::String(s) => format!("'{}'", s.replace('\'', "''")),
ParameterValue::Number(n) => n.to_string(),
ParameterValue::Boolean(b) => {
if *b {
"TRUE".to_string()
} else {
"FALSE".to_string()
}
}
ParameterValue::Array(_) | ParameterValue::Null => {
unreachable!("Grammar prevents arrays and null in literal aesthetic mappings")
}
}
}

/// Determine which columns need casting based on scale requirements.
///
/// For each layer, collects columns that need casting to match the scale's
Expand Down Expand Up @@ -223,6 +205,23 @@ pub fn determine_layer_source(
Some(DataSource::FilePath(path)) => {
format!("'{}'", path)
}
Some(DataSource::Annotation(n)) => {
// Annotation layer - generate a dummy table with n rows.
// The execution pipeline expects all layers to have a DataFrame, even though
// SETTING literals eventually render as Vega-Lite datum values ({"value": ...})
// that don't reference the data. The n-row dummy satisfies schema detection,
// type resolution, and other intermediate steps that expect data to exist.
if *n == 1 {
"(SELECT 1 AS __ggsql_dummy__)".to_string()
} else {
// Generate VALUES clause with n rows: (VALUES (1), (2), ..., (n)) AS t(col)
let rows: Vec<String> = (1..=*n).map(|i| format!("({})", i)).collect();
format!(
"(SELECT * FROM (VALUES {}) AS t(__ggsql_dummy__))",
rows.join(", ")
)
}
}
None => {
// Layer uses global data - caller must ensure has_global is true
debug_assert!(has_global, "Layer has no source and no global data");
Expand Down
5 changes: 3 additions & 2 deletions src/execute/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{naming, DataFrame, GgsqlError, Result};
use polars::prelude::DataType;
use std::collections::{HashMap, HashSet};

use super::casting::{literal_to_sql, TypeRequirement};
use super::casting::TypeRequirement;
use super::schema::build_aesthetic_schema;

/// Build the source query for a layer.
Expand Down Expand Up @@ -90,7 +90,7 @@ pub fn build_layer_select_list(
}
AestheticValue::Literal(lit) => {
// Literals become columns with prefixed aesthetic name
format!("{} AS \"{}\"", literal_to_sql(lit), aes_col_name)
format!("{} AS \"{}\"", lit.to_sql(), aes_col_name)
}
};

Expand Down Expand Up @@ -498,6 +498,7 @@ where
name: prefixed_name,
original_name,
is_dummy,
is_scaled: true,
};
layer.mappings.insert(aesthetic.clone(), value);
}
Expand Down
Loading