Skip to content

Commit 5977c1d

Browse files
authored
Last batch of housekeeping (#265)
* Fix vertical width in boxplots * Fix #248 * minor tweaks to docs * Fix empty terminal bins * reformat
1 parent 26ea615 commit 5977c1d

7 files changed

Lines changed: 71 additions & 36 deletions

File tree

doc/syntax/coord/polar.qmd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ DRAW bar
6363
PROJECT TO polar
6464
SETTING start => -90, end => 90
6565
```
66-
This creates a gauge chart spanning from the 9 o'clock to 3 o'clock position (a 180° arc at the top).
66+
67+
This creates a gauge chart spanning from the 9 o'clock to 3 o'clock position (a 180° arc at the top). Note that scale expansion is applied by default so the data does not fill the whole half-circle.
6768

6869
### Three-quarter pie chart
6970
```{ggsql}

doc/syntax/layer/type/point.qmd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ DRAW point
5757
FILTER sex = 'female'
5858
```
5959

60+
When points are plotted on a discrete scale you will likely see a lot of overplotting. Use jitter position to introduce a bit of random offset to counter that.
61+
6062
```{ggsql}
6163
VISUALISE species AS x, sex AS y, island AS fill FROM ggsql:penguins
6264
DRAW point

doc/syntax/scale/aesthetic/0_position.qmd

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
title: "Position"
33
---
44

5-
Position aesthetics (`x` and `y`, plus all their variants) relate data to placement in the coordinate system of the plot. All layers need at least one of each positional aesthetic mapped in order to show its data. However, the layer may compute positional aesthetics from the mapping. For example, a bar plot calculates the `y` aesthetic by counting the number of records in each group.
5+
Position aesthetics relate data to placement in the coordinate system of the plot. All layers need at least one of each positional aesthetic mapped in order to show its data. However, the layer may compute positional aesthetics from the mapping. For example, a bar plot calculates the `y` aesthetic by counting the number of records in each group defined by the mapped `x` aesthetic.
6+
7+
In the above we use `x`and `y` as examples of position aesthetics but in reality the position aesthetic names are defined by the coordinate system in use. A Cartesian coordinate system recognizes `x` and `y` whereas a polar coordinate system recognizes `radius` and `angle`. You can implicitly choose the coordinate system by mapping to the aesthetics that it uses, i.e. if you map to `radius` and `angle` then a polar coordinate system will be chosen for you.
68

79
## Literal values
810
Scales for position aesthetics never use an output range and always relate to the input range. This is a practical decision by ggsql because different writers may treat the positional aesthetic in different ways. ^[In reality one could easily think of positional literal values as either normalized position along the x or y axis, or absolute units of distance from the bottom left corner of the coordinate system.]
@@ -12,14 +14,10 @@ The lack of true literal values in position means that it is currently hard to p
1214
:::
1315

1416
## Aesthetic families
15-
Positional aesthetics consist of two families: The `x` and `y` family. Each of these consist of their primary aesthetic along with a range of sub aesthetic defined by their suffix:
17+
Positional aesthetics have variants defined by a suffix attached to the primary name (e.g. `x`). The recognized suffixes are:
1618

17-
* `2`
1819
* `end`
1920
* `min`
2021
* `max`
2122

22-
Which version of aesthetic to use depends on the layer, but all aesthetics within a family is scaled by the same scale, which is named after its primary aesthetic. This means that even when rendering a layer that only uses `xmin` and `xmax`, you will still scale it by writing `SCALE x ...` and label it by writing `LABEL x => ...`
23-
24-
## Coordinate system
25-
Another thing that makes positional aesthetics different from other aesthetic is that they are dependent on a coordinate system which takes position scales and defines how values should be converted to a location on a plane. The default Cartesian coordinate system does what is generally expected: it places points along two perpendicular axes in a 2D plane. Other systems such as polar coordinates may dramatically change the look of a layer, transforming both the straightness of lines and positional relation of data.
23+
Which version of an aesthetic to use depends on the layer, but all aesthetics within a family is scaled by the same scale, which is named after its primary aesthetic. This means that even when rendering a layer that only uses `xmin` and `xmax`, you will still scale it by writing `SCALE x ...` and label it by writing `LABEL x => ...`

doc/syntax/scale/aesthetic/Z_faceting.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Faceting
44

55
ggsql provides one or two aesthetics related to faceting. These are special in the sense that they do not alter the display of the single data values, but rather alter in which plot they appear. While it is possible to map to these aesthetics in a layer they are most often applied globally as part of the [`FACET` clause](../../clause/facet.qmd).
66

7-
The aesthetics provided are either `panel` (for 1-dimensional faceting) or `row` and `column` (for 2-dimensional faceting). These aesthetics have to compatible with the facet clause: it is not possible to map to `panel` in a 2-dimensional faceting plot nor is it possible to map `row` and `column` in a 1-dimensional plot.
7+
The aesthetics provided are either `panel` (for 1-dimensional faceting) or `row` and `column` (for 2-dimensional faceting). These aesthetics have to be compatible with the facet clause: it is not possible to map to `panel` in a 2-dimensional faceting plot nor is it possible to map `row` and `column` in a 1-dimensional plot.
88

99
## Literal values
1010
Scales for facet aesthetics never use an output range and always relate to the input range. This means that no concept of literal values applies.

doc/syntax/scale/type/binned.qmd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ If `oob => 'squish'` then the terminal labels are formatted as e.g. "≥ 10" to
167167
```{ggsql}
168168
VISUALISE bill_dep AS x FROM ggsql:penguins
169169
DRAW bar
170-
SCALE BINNED x
171-
RENAMING 50 => 'Fifty'
170+
SCALE BINNED x
171+
RENAMING 20 => 'Twenty'
172172
```
173173

174174
#### Adding suffix to break labels

src/plot/scale/scale_type/binned.rs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,15 @@ impl ScaleTypeTrait for Binned {
332332
match scale.properties.get("breaks") {
333333
Some(ParameterValue::Number(_)) => {
334334
// Scalar count → calculate actual breaks and store as Array
335-
if let Some(breaks) = self.resolve_breaks(
336-
scale.input_range.as_deref(),
337-
&scale.properties,
338-
scale.transform.as_ref(),
339-
) {
335+
// Use raw data range (not expanded input_range) so breaks align
336+
// to actual data extent; expansion happens later in step 5b.
337+
let break_range = match &context.range {
338+
Some(InputRange::Continuous(r)) => Some(r.as_slice()),
339+
_ => scale.input_range.as_deref(),
340+
};
341+
if let Some(breaks) =
342+
self.resolve_breaks(break_range, &scale.properties, scale.transform.as_ref())
343+
{
340344
// For binned implicit, keep all breaks (they extend past data).
341345
// For binned explicit, filter to input range.
342346
let filtered = if binned_implicit {
@@ -387,7 +391,13 @@ impl ScaleTypeTrait for Binned {
387391
};
388392

389393
if let Some(interval) = TemporalInterval::create_from_str(interval_str) {
390-
if let Some(ref range) = scale.input_range {
394+
// Use raw data range (not expanded input_range) so breaks align
395+
// to actual data extent; expansion happens later in step 5b.
396+
let break_range: Option<&[ArrayElement]> = match &context.range {
397+
Some(InputRange::Continuous(r)) => Some(r.as_slice()),
398+
_ => scale.input_range.as_deref(),
399+
};
400+
if let Some(range) = break_range {
391401
let breaks: Vec<ArrayElement> = match resolved_transform.transform_kind() {
392402
TransformKind::Date => {
393403
let min = range[0].to_f64().unwrap_or(0.0) as i32;
@@ -422,10 +432,18 @@ impl ScaleTypeTrait for Binned {
422432
.iter()
423433
.map(|elem| resolved_transform.parse_value(elem))
424434
.collect();
425-
// Filter to input range
426-
let filtered = super::super::super::breaks::filter_breaks_to_range(
427-
&converted, range,
428-
);
435+
// Only filter to input range when user provided explicit FROM clause
436+
let filtered = if scale.explicit_input_range {
437+
if let Some(ref ir) = scale.input_range {
438+
super::super::super::breaks::filter_breaks_to_range(
439+
&converted, ir,
440+
)
441+
} else {
442+
converted
443+
}
444+
} else {
445+
converted
446+
};
429447
scale
430448
.properties
431449
.insert("breaks".to_string(), ParameterValue::Array(filtered));

src/writer/vegalite/layer.rs

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,20 +1540,20 @@ impl GeomRenderer for ErrorBarRenderer {
15401540
let mut layers = vec![layer_spec.clone()];
15411541

15421542
// Determine if this is a vertical or horizontal error bar and set up parameters
1543-
let is_vertical = layer_spec["encoding"]["x2"].is_null();
1543+
let is_vertical = !is_transposed(layer);
15441544
let (orient, position, min_field, max_field) = if is_vertical {
15451545
(
15461546
"horizontal",
15471547
"y",
1548-
naming::aesthetic_column("ymin"),
1549-
naming::aesthetic_column("ymax"),
1548+
naming::aesthetic_column("pos2min"),
1549+
naming::aesthetic_column("pos2max"),
15501550
)
15511551
} else {
15521552
(
15531553
"vertical",
15541554
"x",
1555-
naming::aesthetic_column("xmin"),
1556-
naming::aesthetic_column("xmax"),
1555+
naming::aesthetic_column("pos1min"),
1556+
naming::aesthetic_column("pos1max"),
15571557
)
15581558
};
15591559

@@ -1817,11 +1817,19 @@ impl BoxplotRenderer {
18171817
let mut box_part = create_layer(
18181818
&summary_prototype,
18191819
"box",
1820-
json!({
1821-
"type": "bar",
1822-
"width": width_value,
1823-
"align": "center"
1824-
}),
1820+
if is_horizontal {
1821+
json!({
1822+
"type": "bar",
1823+
"height": width_value,
1824+
"baseline": "middle"
1825+
})
1826+
} else {
1827+
json!({
1828+
"type": "bar",
1829+
"width": width_value,
1830+
"align": "center"
1831+
})
1832+
},
18251833
);
18261834
box_part["encoding"][value_var1] = y_encoding.clone();
18271835
box_part["encoding"][value_var2] = y2_encoding.clone();
@@ -1830,11 +1838,19 @@ impl BoxplotRenderer {
18301838
let mut median_line = create_layer(
18311839
&summary_prototype,
18321840
"median",
1833-
json!({
1834-
"type": "tick",
1835-
"width": width_value,
1836-
"align": "center"
1837-
}),
1841+
if is_horizontal {
1842+
json!({
1843+
"type": "tick",
1844+
"height": width_value,
1845+
"baseline": "middle"
1846+
})
1847+
} else {
1848+
json!({
1849+
"type": "tick",
1850+
"width": width_value,
1851+
"align": "center"
1852+
})
1853+
},
18381854
);
18391855
median_line["encoding"][value_var1] = y_encoding;
18401856

0 commit comments

Comments
 (0)