Commit 63492a8
Text layer (#155)
* Add fontsize aesthetic for linear text sizing
Introduces a separate 'fontsize' aesthetic as an alternative to 'size' for
text/label geoms. Unlike 'size' (which uses area-based scaling with radius²
conversion for point marks), 'fontsize' uses linear scaling for font sizes.
Changes:
- Grammar: Add 'fontsize' to aesthetic names
- Geoms: Add 'fontsize' to Text and Label supported aesthetics
- Aesthetics: Register 'fontsize' in NON_POSITIONAL list
- Writer: Map 'fontsize' → 'size' channel in Vega-Lite output
- Scale: Add default range [8.0, 20.0] for fontsize aesthetic
- Tests: Add test_fontsize_linear_scaling integration test
Usage:
DRAW text MAPPING x AS x, y AS y, value AS fontsize
SCALE fontsize TO [10, 20] -- Linear: 10pt to 20pt (not area-converted)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Implement TextRenderer with data-splitting for font properties
Add TextRenderer implementation that handles font aesthetics (family,
fontface, hjust, vjust) by splitting data into multiple Vega-Lite layers
when font properties vary across rows.
Key features:
- Single-layer optimization: When all fonts are constant, generates one
layer with mark properties set directly
- Multi-layer splitting: When fonts vary, creates one layer per unique
font combination while preserving ORDER BY
- Proper SOURCE_COLUMN filtering: Uses empty string for single-layer
and suffix keys for multi-layer to match BoxplotRenderer pattern
- Font property mapping:
- family → mark.font
- fontface → mark.fontWeight/fontStyle
- hjust → mark.align
- vjust → mark.baseline
Tests included for both constant and varying font cases.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Simplify FontStrategy by unifying single and multi-layer cases
Remove the FontStrategy enum variants and use a single struct with a
groups vector. The single-layer case now has 1 group containing all rows,
while the multi-layer case has N groups.
Benefits:
- Eliminates redundant code paths (no more match statements)
- Simpler prepare_data() - just iterate over groups
- Simpler finalize() - unified layer generation logic
- Fewer lines of code overall
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Remove TextMetadata wrapper, use FontStrategy directly
TextMetadata was simply wrapping FontStrategy with no additional value.
Store FontStrategy directly in PreparedData metadata instead.
This eliminates 4 lines and one level of indirection.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Remove unused signature field from FontGroup
The signature field was only used during group construction as a
HashMap key to track row assignments. After groups are built, the
field was never accessed (marked with #[allow(dead_code)]).
Removed the field and its assignments, keeping the local signature
variable for grouping logic.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Simplify TextRenderer by using HashMap for grouping
Eliminated FontGroup struct and common_properties field by:
- Using HashMap<String, (properties, indices)> for grouping during
construction, then converting to sorted Vec
- Storing all properties (constant + varying) in each group's HashMap
- Using plain tuples (HashMap<String, Value>, Vec<usize>) instead of
a dedicated struct
This reduces code by 24 net lines while maintaining the same
functionality. Properties are now the HashMap keys (via signature)
and row indices are values, making the data structure more direct.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Remove FontStrategy wrapper struct
FontStrategy was just wrapping a single Vec. Eliminated it by:
- Returning Vec<(HashMap<String, Value>, Vec<usize>)> directly from
analyze_font_columns()
- Storing the Vec directly as metadata in PreparedData::Composite
- Downcasting to Vec type directly in finalize()
This removes 7 net lines while maintaining identical functionality.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Use HashMap<FontKey, Vec<usize>> with direct property conversion
Refactored TextRenderer to use FontKey tuple containing converted
Vega-Lite Values instead of intermediate structures:
- FontKey = (family, fontWeight, fontStyle, align, baseline) as Values
- convert_fontface returns (fontWeight, fontStyle) tuple
- Properties converted once during grouping (in analyze_font_columns)
- finalize_layers directly inserts Values into mark object
- Eliminated font_key_to_properties, apply_mark_property, and
map_aesthetic_to_mark_property helpers
Benefits:
- No string signatures or intermediate HashMaps
- Properties converted once per unique combination (not per row)
- Simpler finalize_layers with direct value insertion
- No special-case spreading logic for fontface
This removes 70 net lines while maintaining identical functionality.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Sort font groups once in analyze_font_columns
Changed analyze_font_columns to return Vec<(FontKey, Vec<usize>)>
instead of HashMap, with sorting done once at the end of grouping.
Before: HashMap was sorted twice - once in prepare_data() and again
in finalize_layers() to maintain consistent ordering.
After: Groups are sorted once after HashMap construction in
analyze_font_columns(), then both prepare_data() and finalize_layers()
iterate the pre-sorted Vec directly.
This preserves HashMap's O(1) insertion benefit during construction
while eliminating redundant sort operations.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Use Option<Value> for family and apply clippy suggestions
Changes:
- convert_family() returns Option<Value> instead of Value
- Returns None for empty family strings
- Simplifies finalize_layers to use if let Some(family_val)
- Apply clippy suggestion: use or_default() instead of or_insert_with(Vec::new)
This eliminates the is_none_or check and makes the intent clearer:
family is optional and should be omitted from the mark object when
not specified.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Split non-contiguous indices to preserve z-order
When font groups have non-contiguous row indices (e.g., [0, 2, 5, 6]),
split them into separate contiguous ranges ([0], [2], [5, 6]) to
preserve rendering order.
Example:
- Row 0: Arial "A"
- Row 1: Courier "B"
- Row 2: Arial "C"
Before: Arial layer renders A and C together, then B on top
After: Three layers render in order: A, then B, then C
This ensures that the DRAW clause ORDER BY is respected for z-order
stacking, even when rows with the same font properties are
interleaved with rows having different properties.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Suppress legend and scale for text encoding
The label aesthetic (mapped to Vega-Lite 'text' encoding) should not
generate a legend or scale, as text values are literal display strings
rather than data values that need scaling or legend representation.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Refactor TextRenderer to use nested layers with shared encoding
Changes:
- Use nested layer structure for multi-group text rendering
- Single group: returns one layer with full encoding
- Multiple groups: returns parent layer with shared encoding,
child layers only have mark + transform
- Extract helper functions for code reuse:
- apply_font_properties: applies font properties to mark object
- build_transform_with_filter: creates transform with source filter
- Both finalize_single_layer and finalize_nested_layers now use
helpers to avoid duplication
This approach eliminates duplicate encoding specifications in
multi-layer output while preserving z-order through contiguous
range splitting.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add test for text renderer nested layers structure
- Verifies nested layer structure is correct for multiple font groups
- Tests that parent spec has shared encoding
- Tests that child layers only have mark + transform
- Tests that font properties are applied to mark objects
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Unify single and nested layer logic in TextRenderer
Changes:
- Remove finalize_single_layer function
- Always use nested layer structure (works for 1 or N groups)
- Simplify prepare_data to always use _font_N suffix
- Update test expectations
This eliminates code duplication and special-case handling for
single-group scenarios, reducing implementation by ~24 lines.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add angle aesthetic to text geom
Changes:
- Add 'angle' to supported aesthetics in Text geom
- Update FontKey tuple to include angle (6th element)
- Extract angle column in analyze_font_columns
- Add convert_angle function (parses numeric angle in degrees)
- Apply angle property in apply_font_properties
- Remove angle from encoding in modify_encoding
The angle aesthetic is now handled the same way as other font
properties (family, fontface, hjust, vjust) via data-splitting,
since Vega-Lite requires it as a mark property.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Complete angle aesthetic implementation with integration test
This commit completes the angle aesthetic implementation:
Grammar changes:
- Add 'angle' to aesthetic keywords in tree-sitter grammar
Label geom consistency:
- Add 'angle' to supported aesthetics in Label geom
- Brings label geom in line with text geom support
TextRenderer improvements:
- Fix convert_angle to handle both numeric and string columns
- Add angle normalization to [0, 360) range
- Handle integer, float, and string angle values
Integration test:
- Add test_text_angle_integration for full SQL → Vega-Lite pipeline
- Verifies nested layer structure with angle mark properties
- Tests angle normalization and data splitting
- Validates non-contiguous index handling
The angle aesthetic now works end-to-end: SQL query with angle
column → TextRenderer splits data by unique angles → Vega-Lite
generates nested layers with angle mark properties.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Refactor TextRenderer to use pure run-length encoding
Replace the group-sort-split approach with elegant run-length encoding
for handling font property variations in text layers.
Changes:
Algorithm improvement:
- Replace HashMap grouping + sorting + contiguous splitting with
single-pass RLE scan
- Complexity: O(n log n) → O(n)
- Memory: 8n bytes per run → 16 bytes per run
Type simplification:
- Before: Vec<(FontKey, Vec<usize>)> - explicit row indices
- After: Vec<(FontKey, usize)> - run lengths with implicit positions
- Start positions derived from cumulative run lengths
DataFrame operations:
- Replace boolean masking (filter_by_indices) with direct slicing
- Use df.slice(position, length) - O(1) pointer arithmetic
- Remove filter_by_indices helper function entirely
Function rename:
- analyze_font_columns() → build_font_rle()
- Clearer name indicating RLE technique and output type
Benefits:
- 28 net lines removed (52 insertions, 80 deletions)
- Simpler single-pass algorithm
- More efficient memory usage
- Faster DataFrame operations
- All tests pass unchanged
The refactoring maintains identical behavior while using the canonical
run-length encoding pattern for grouping consecutive rows.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add nudge_x and nudge_y parameters to text/label geoms
Add nudge parameters that map to Vega-Lite's xOffset/yOffset mark
properties, allowing fine-grained positioning adjustments for text labels.
Changes:
Text and Label geoms:
- Add nudge_x and nudge_y to default_params
- Default to Null (not applied unless explicitly set)
TextRenderer:
- Build base mark prototype with nudge offsets (if specified)
- Clone and extend with font properties for each run
- Pass layer to finalize_nested_layers for parameter access
Integration test:
- Verify nudge_x → xOffset and nudge_y → yOffset mapping
- Confirm parameters apply to all nested text layers
Usage:
DRAW text SETTING nudge_x => 5, nudge_y => -10
This enables fine-tuning text label positions without modifying
the underlying x/y data, useful for avoiding overlaps or improving
label placement in dense visualizations.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add format parameter for text label formatting
Add template-based label formatting to text/label geoms, reusing the
existing format.rs infrastructure from SCALE RENAMING.
Changes:
format.rs improvements:
- Add format_dataframe_column() - clean API for DataFrame column formatting
- Refactor to convert columns to strings first, then apply formatting
- Add format_value() helper shared by both APIs
- Improved error message showing actual datatype for unsupported types
- Two-step process: column→string, then template application
Text/Label geoms:
- Add 'format' parameter (defaults to Null)
- Works with both geoms for consistency
TextRenderer:
- Add apply_label_formatting() helper
- Apply formatting in prepare_data() before font analysis
- Pass layer parameter through prepare_data() trait method
- Update all GeomRenderer implementations
Integration tests:
- test_text_label_formatting - Title case transformation
- test_text_label_formatting_numeric - Printf-style number formatting
Supported placeholder syntax:
- {} - Plain insertion
- {:UPPER} - Uppercase
- {:lower} - Lowercase
- {:Title} - Title Case
- {:time %fmt} - DateTime strftime format
- {:num %fmt} - Number printf format
Usage:
DRAW text SETTING format => 'Region: {:Title}'
DRAW text SETTING format => '${:num %.2f}'
DRAW text SETTING format => '{:time %b %Y}'
The format parameter transforms label values before rendering, enabling
clean label presentation without modifying source data.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* soothe compiler
* Handle font properties from parameters
* Refactor text geom font property handling
- Separate value selection from conversion in all convert functions
- Use early returns with ? operator for cleaner control flow
- Inline convert function calls to eliminate intermediate variables
- Change property insertion to use if let Some with .insert()
- Fix column lookup to use naming::aesthetic_column()
- Optimize angle extraction to handle numeric columns without cast->parse
- Remove unused FontKey type alias
- Fix test_fontsize_linear_scaling to include required label aesthetic
All text rendering tests passing (11/11).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* specify fontsize in pt
* delenda est
* docs
* fix mismerged test
* fix another test expectation
* finally do something about this darn test that keeps mucking up test results on my machine
* soothe clippy
* update docs
* exclude label as group aesthetic
* move label formatting to post_process method
* eradicate label layer
* divorce 'fontface' into 'fontweight' and 'italic'
* rename `nudge_x/y` to `offset_x/y`
* cargo fmt
* Update doc/syntax/layer/type/text.qmd
Co-authored-by: Thomas Lin Pedersen <thomasp85@gmail.com>
* Combine offset_x and offset_y into single (array) offset
* fancy approach to fontweight
* rename `family` to `typeface`
* rename angle to rotation
* cargo fmt
* soothe clippy
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Thomas Lin Pedersen <thomasp85@gmail.com>1 parent 09ab79a commit 63492a8
21 files changed
Lines changed: 1864 additions & 107 deletions
File tree
- doc
- syntax
- layer/type
- ggsql-vscode/syntaxes
- src
- execute
- parser
- plot
- layer/geom
- scale/scale_type
- reader
- writer/vegalite
- tree-sitter-ggsql
- queries
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
334 | 334 | | |
335 | 335 | | |
336 | 336 | | |
337 | | - | |
| 337 | + | |
338 | 338 | | |
339 | 339 | | |
340 | 340 | | |
| |||
1211 | 1211 | | |
1212 | 1212 | | |
1213 | 1213 | | |
1214 | | - | |
| 1214 | + | |
1215 | 1215 | | |
1216 | 1216 | | |
1217 | 1217 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | | - | |
172 | | - | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
173 | 174 | | |
174 | 175 | | |
175 | 176 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
| 252 | + | |
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
687 | 687 | | |
688 | 688 | | |
689 | 689 | | |
690 | | - | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
691 | 693 | | |
| 694 | + | |
| 695 | + | |
692 | 696 | | |
693 | 697 | | |
694 | 698 | | |
| |||
698 | 702 | | |
699 | 703 | | |
700 | 704 | | |
701 | | - | |
702 | | - | |
| 705 | + | |
| 706 | + | |
703 | 707 | | |
704 | 708 | | |
705 | 709 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
182 | | - | |
183 | 182 | | |
184 | | - | |
185 | | - | |
186 | | - | |
187 | | - | |
188 | | - | |
189 | | - | |
190 | | - | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
199 | 186 | | |
200 | 187 | | |
201 | 188 | | |
202 | 189 | | |
203 | 190 | | |
204 | 191 | | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
205 | 281 | | |
206 | 282 | | |
207 | 283 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
611 | 611 | | |
612 | 612 | | |
613 | 613 | | |
614 | | - | |
615 | 614 | | |
616 | 615 | | |
617 | 616 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
75 | | - | |
76 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
77 | 79 | | |
78 | 80 | | |
79 | 81 | | |
| |||
0 commit comments