Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0eacdf2
Refactor scale constructors: rename new to configured
jonmmease Jun 28, 2025
d4f41bb
Document configuration options for all scale types
jonmmease Jun 28, 2025
b4c7e4f
Add scale_type() method to ScaleImpl trait
jonmmease Jun 29, 2025
4942122
Add array-based invert() method to scales
jonmmease Jun 30, 2025
ee7908e
Add normalize() method to ConfiguredScale for applying nice domain tr…
jonmmease Jul 9, 2025
6d903f9
Add zero option support to all scale types with normalization
jonmmease Jul 9, 2025
bc05a9c
feat: Add initial TimeScale implementation for temporal data
jonmmease Jul 10, 2025
e01b77f
ignore tasks dir
jonmmease Jul 10, 2025
64ef64a
feat: Add timezone support and calendar-aware operations to TimeScale
jonmmease Jul 10, 2025
8c49cbb
feat: Implement invert() method for TimeScale
jonmmease Jul 10, 2025
f417f33
feat: Implement comprehensive DST support for TimeScale
jonmmease Jul 10, 2025
9871ae9
feat: Implement DST Phase 5 - Update scale operations for DST awareness
jonmmease Jul 10, 2025
0cb9955
docs: Update time scale implementation plan to reflect DST Phase 5 co…
jonmmease Jul 10, 2025
11ecd92
fix: Remove time scale plan from git tracking
jonmmease Jul 10, 2025
4e089e5
feat: Implement tick formatting for time scale
jonmmease Jul 10, 2025
cbbaaba
fix: Implement proper round option support for band scales
jonmmease Jul 12, 2025
74e40ba
formatting
jonmmease Jul 12, 2025
35df700
fix: Correct band scale rounding to fully match Vega behavior
jonmmease Jul 12, 2025
f8330ba
support general padding
jonmmease Jul 12, 2025
fa3f019
Add scale option validation system
jonmmease Jul 12, 2025
bb1f172
feat: Add padding support for quantitative scales
jonmmease Jul 19, 2025
cacb1a3
fix: Only add supported options during scale normalization
jonmmease Jul 19, 2025
e621f4c
fix: Only add supported options during scale normalization
jonmmease Jul 19, 2025
e99f484
style: Apply cargo fmt formatting
jonmmease Jul 21, 2025
c673a5e
fix: Update CI workflow to fix build failures
jonmmease Jul 21, 2025
3e15b36
fix: Use inline format args to fix clippy warning
jonmmease Jul 21, 2025
397cec4
fix: Use inline format args throughout codebase
jonmmease Jul 21, 2025
9fe6aae
style: Apply cargo fmt
jonmmease Jul 21, 2025
1141f4c
fix: Use inline format args in example projects
jonmmease Jul 21, 2025
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
24 changes: 24 additions & 0 deletions .claude/commands/precommit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
allowed-tools: Bash(git add:*), Bash(git status:*)
description: Prepare changes for commit
---

## Context

- Current git status: !`git status`
- Current git diff (staged and unstaged changes): !`git diff HEAD`
- Current branch: !`git branch --show-current`
- Recent commits: !`git log --oneline -10`

## Your task

Review and improve the uncommitted changes. Do not make changes to code that was not modified since the last commit.

- Review NEW comments for accuracy. Make sure they refer to the final state of the code, and not to steps along the way
- Remove NEW dead code, include unused functions, unused function arguments, and unused variables
- Run NEW tests
- Perform formatting and fix lints
- Perform type checking
- Rerun formatting after fixing type check errors and make sure there are no errors
- `git add` changed files
- Draft and display a commit message. Do not attempt to commit changes yourself
27 changes: 27 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"permissions": {
"allow": [
"Bash(cargo:*)",
"Bash(rg:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(sed:*)",
"WebFetch(domain:docs.rs)",
"Bash(wasm-pack build:*)",
"WebFetch(domain:crates.io)",
"Bash(git add:*)",
"Bash(pixi run:*)",
"Bash(sg:*)",
"Bash(ls:*)",
"Bash(gh pr checks:*)",
"Bash(gh run view:*)",
"Bash(RUSTFLAGS=\"-D warnings\" cargo clippy)",
"Bash(git submodule:*)",
"mcp__rust-analyzer__rename",
"Bash(RUSTFLAGS=\"-D warnings\" cargo check --tests)",
"WebFetch(domain:github.com)",
"Bash(gh pr view:*)"
]
},
"enableAllProjectMcpServers": false
}
7 changes: 1 addition & 6 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@ jobs:
run: cargo clippy
- name: Build
run: |
cd avenger-scenegraph && cargo build && cd ..
cd avenger-vega && cargo build && cd ..
cd avenger-wgpu && cargo build && cd ..
cd avenger-vega-test-data && cargo build && cd ..
pushd examples/scatter-panning && cargo build && popd
pushd examples/wgpu-winit && cargo build && popd
cargo build --workspace
- name: Run tests (excluding GPU tests)
run: |
cargo test --workspace --verbose \
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ examples/iris-pan-zoom/geometry.svg

avenger-lang-preview/preview.avgr
/avenger-vega-test-data/target/
/tasks/
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion avenger-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ where
{
Ok(scene_graph) => scene_graph,
Err(e) => {
eprintln!("Failed to build scene graph: {:?}", e);
eprintln!("Failed to build scene graph: {e:?}");
return Err(AvengerAppError::InternalError(
"Failed to build scene graph".to_string(),
));
Expand Down
3 changes: 1 addition & 2 deletions avenger-image/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ impl RgbaImage {
Ok(Self::from_image(&img.into_rgba8()))
} else {
Err(AvengerImageError::InternalError(format!(
"Unsupported image URL: {}",
s
"Unsupported image URL: {s}"
)))
}
}
Expand Down
1 change: 1 addition & 0 deletions avenger-scales/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
paste = { workspace = true }
strum = { workspace = true }
lazy_static = { workspace = true }

[dev-dependencies]
float-cmp = "0.10.0"
Expand Down
7 changes: 4 additions & 3 deletions avenger-scales/examples/categorical_scales.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let categories = vec!["Category A", "Category B", "Category C", "Category D"];
let domain = Arc::new(StringArray::from(categories.clone())) as ArrayRef;

let band_scale = BandScale::new(domain.clone(), (0.0, 400.0))
let band_scale = BandScale::configured(domain.clone(), (0.0, 400.0))
.with_option("padding", 0.1) // 10% padding between bands
.with_option("padding_outer", 0.05); // 5% padding on outer edges

Expand All @@ -33,7 +33,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 2: Point Scale for Scatter Plots
println!("\n2. Point Scale (for scatter plots, no bandwidth):");

let point_scale = PointScale::new(domain.clone(), (0.0, 300.0)).with_option("padding", 0.5); // Space around the points
let point_scale =
PointScale::configured(domain.clone(), (0.0, 300.0)).with_option("padding", 0.5); // Space around the points

let point_positions = point_scale.scale_to_numeric(&test_array)?;
let point_values = point_positions.as_vec(categories.len(), None);
Expand All @@ -53,7 +54,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let stroke_cap_values = vec!["round", "square", "butt"];
let stroke_range = Arc::new(StringArray::from(stroke_cap_values)) as ArrayRef;

let ordinal_scale = OrdinalScale::new(cat_domain).with_range(stroke_range);
let ordinal_scale = OrdinalScale::configured(cat_domain).with_range(stroke_range);

let test_categories = Arc::new(StringArray::from(categories.clone())) as ArrayRef;
let stroke_result = ordinal_scale.scale_to_stroke_cap(&test_categories)?;
Expand Down
6 changes: 4 additions & 2 deletions avenger-scales/examples/color_scales.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
];
let color_range = Scalar::arrays_into_list_array(color_arrays)?;

let linear_color_scale = LinearScale::new((0.0, 10.0), (0.0, 1.0)).with_range(color_range);
let linear_color_scale =
LinearScale::configured((0.0, 10.0), (0.0, 1.0)).with_range(color_range);

let test_values = vec![0.0, 2.5, 5.0, 7.5, 10.0];
let values_array = Arc::new(Float32Array::from(test_values.clone())) as ArrayRef;
Expand Down Expand Up @@ -52,7 +53,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
];
let three_color_range = Scalar::arrays_into_list_array(color_arrays)?;

let log_color_scale = LogScale::new((1.0, 100.0), (0.0, 1.0)).with_range(three_color_range);
let log_color_scale =
LogScale::configured((1.0, 100.0), (0.0, 1.0)).with_range(three_color_range);

let log_test_values = vec![1.0, 3.16, 10.0, 31.6, 100.0]; // Evenly spaced in log space
let log_values_array = Arc::new(Float32Array::from(log_test_values.clone())) as ArrayRef;
Expand Down
6 changes: 3 additions & 3 deletions avenger-scales/examples/formatting_and_ticks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 1: Numeric Formatting
println!("1. Number Formatting:");

let scale = LinearScale::new((0.0, 1000.0), (0.0, 500.0));
let scale = LinearScale::configured((0.0, 1000.0), (0.0, 500.0));

let numbers = vec![0.0, 123.456, 1000.0, 10000.0, 0.00123];
let number_array = Arc::new(Float32Array::from(numbers.clone())) as ArrayRef;
Expand Down Expand Up @@ -110,7 +110,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("\n5. Tick Generation:");

// Linear scale ticks
let tick_scale = LinearScale::new((0.0, 100.0), (0.0, 400.0));
let tick_scale = LinearScale::configured((0.0, 100.0), (0.0, 400.0));

println!("Linear scale ticks (different counts):");
for tick_count in [5.0, 10.0, 15.0] {
Expand All @@ -123,7 +123,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 6: Scale with Custom Options
println!("\n6. Scale with Custom Options:");

let custom_scale = LinearScale::new((0.0, 1000.0), (0.0, 500.0))
let custom_scale = LinearScale::configured((0.0, 1000.0), (0.0, 500.0))
.with_option("clamp", true)
.with_option("round", true)
.with_option("nice", true);
Expand Down
2 changes: 1 addition & 1 deletion avenger-scales/examples/linear_scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Simple Linear Scale Example ===\n");

// Create a linear scale that maps domain [0, 100] to range [0, 500]
let scale = LinearScale::new((0.0, 100.0), (0.0, 500.0));
let scale = LinearScale::configured((0.0, 100.0), (0.0, 500.0));

// Test data: some values in our domain
let test_values = vec![0.0, 25.0, 50.0, 75.0, 100.0];
Expand Down
8 changes: 4 additions & 4 deletions avenger-scales/examples/logarithmic_scales.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 1: Basic Log Scale
println!("1. Log Scale (base 10, domain [1, 1000]):");

let log_scale = LogScale::new((1.0, 1000.0), (0.0, 300.0)).with_option("base", 10.0);
let log_scale = LogScale::configured((1.0, 1000.0), (0.0, 300.0)).with_option("base", 10.0);

let test_values = vec![1.0, 10.0, 100.0, 1000.0];
let values_array = Arc::new(Float32Array::from(test_values.clone())) as ArrayRef;
Expand All @@ -32,7 +32,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 2: Log Scale with Different Base
println!("\n2. Log Scale (base 2, for computer science applications):");

let log2_scale = LogScale::new((1.0, 64.0), (0.0, 200.0)).with_option("base", 2.0);
let log2_scale = LogScale::configured((1.0, 64.0), (0.0, 200.0)).with_option("base", 2.0);

let powers_of_2 = vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0];
let pow2_array = Arc::new(Float32Array::from(powers_of_2.clone())) as ArrayRef;
Expand All @@ -49,7 +49,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("\n3. Symlog Scale (handles negative values and zero):");

let symlog_scale =
SymlogScale::new((-1000.0, 1000.0), (0.0, 400.0)).with_option("constant", 100.0); // Linear region around zero: [-100, 100]
SymlogScale::configured((-1000.0, 1000.0), (0.0, 400.0)).with_option("constant", 100.0); // Linear region around zero: [-100, 100]

let symlog_values = vec![-1000.0, -100.0, -10.0, 0.0, 10.0, 100.0, 1000.0];
let symlog_array = Arc::new(Float32Array::from(symlog_values.clone())) as ArrayRef;
Expand All @@ -65,7 +65,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 4: Power Scale (square root, good for area mappings)
println!("\n4. Power Scale (square root, for area-based visualizations):");

let sqrt_scale = PowScale::new((0.0, 100.0), (0.0, 200.0)).with_option("exponent", 0.5); // Square root
let sqrt_scale = PowScale::configured((0.0, 100.0), (0.0, 200.0)).with_option("exponent", 0.5); // Square root

let area_values = vec![
0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0,
Expand Down
9 changes: 5 additions & 4 deletions avenger-scales/examples/quantile_threshold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

let grade_range = Arc::new(StringArray::from(vec!["F", "D", "C", "B", "A"])) as ArrayRef;

let quantile_scale = QuantileScale::new(test_scores.clone(), grade_range);
let quantile_scale = QuantileScale::configured(test_scores.clone(), grade_range);

// Test with some sample scores
let sample_scores = vec![50.0, 65.0, 75.0, 85.0, 95.0];
Expand All @@ -38,7 +38,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("\n2. Quantize Scale (uniform intervals):");

let grade_range2 = Arc::new(StringArray::from(vec!["F", "D", "C", "B", "A"])) as ArrayRef;
let quantize_scale = QuantizeScale::new((0.0, 100.0), grade_range2).with_option("nice", true);
let quantize_scale =
QuantizeScale::configured((0.0, 100.0), grade_range2).with_option("nice", true);

let quantize_result = quantize_scale.scale_to_string(&sample_array)?;
let quantize_grades = quantize_result.as_vec(sample_scores.len(), None);
Expand All @@ -55,7 +56,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let thresholds = vec![60.0, 70.0, 80.0, 90.0];
let grade_range3 = Arc::new(StringArray::from(vec!["F", "D", "C", "B", "A"])) as ArrayRef;

let threshold_scale = ThresholdScale::new(thresholds, grade_range3);
let threshold_scale = ThresholdScale::configured(thresholds, grade_range3);

let threshold_result = threshold_scale.scale_to_string(&sample_array)?;
let threshold_grades = threshold_result.as_vec(sample_scores.len(), None);
Expand All @@ -76,7 +77,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let risk_categories =
Arc::new(StringArray::from(vec!["Low", "Medium", "High", "Critical"])) as ArrayRef;

let risk_scale = ThresholdScale::new(risk_thresholds, risk_categories);
let risk_scale = ThresholdScale::configured(risk_thresholds, risk_categories);

let risk_result = risk_scale.scale_to_string(&risk_array)?;
let risk_labels = risk_result.as_vec(risk_values.len(), None);
Expand Down
12 changes: 12 additions & 0 deletions avenger-scales/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ pub enum AvengerScaleError {
#[error("Invalid timezone: {0}")]
InvalidTimezone(String),

#[error("Invalid timezone: {0}")]
InvalidTimezoneError(String),

#[error("Invalid data type {0} for {1} scale")]
InvalidDataTypeError(arrow::datatypes::DataType, String),

#[error("Not implemented: {0}")]
NotImplementedError(String),

#[error("DST transition error: {0}")]
DstTransitionError(String),

#[error("Invalid SVG transform string: {0}")]
InvalidSvgTransformString(#[from] svgtypes::Error),

Expand Down
19 changes: 8 additions & 11 deletions avenger-scales/src/format_num/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,9 @@ impl NumberFormat {
} else {
significant_digits.unwrap() - 1
};
format!("{:.1$e}", value, precision)
format!("{value:.precision$e}")
} else {
format!("{:e}", value)
format!("{value:e}")
};

let exp_tokens: Vec<&str> = formatted_value.split('e').collect::<Vec<&str>>();
Expand Down Expand Up @@ -402,7 +402,7 @@ impl NumberFormat {
precision: usize,
include_decimal_point: bool,
) -> String {
let formatted = format!("{:.1$e}", value, precision);
let formatted = format!("{value:.precision$e}");
let tokens = formatted.split(format_type).collect::<Vec<&str>>();

let exp_suffix = if &tokens[1][0..1] == "-" {
Expand Down Expand Up @@ -546,11 +546,8 @@ impl NumberFormat {
}

// Compute the prefix and suffix.
let prefix = format!("{}{}", sign_prefix, leading_part);
let suffix = format!(
"{}{}{}",
decimal_part, si_prefix_exponent, unit_of_measurement
);
let prefix = format!("{sign_prefix}{leading_part}");
let suffix = format!("{decimal_part}{si_prefix_exponent}{unit_of_measurement}");

// If should group and filling character is different than "0",
// group digits before applying padding.
Expand Down Expand Up @@ -580,8 +577,8 @@ impl NumberFormat {
};

match format_spec.align {
Some("<") => format!("{}{}{}{}", prefix, value, suffix, padding),
Some("=") => format!("{}{}{}{}", prefix, padding, value, suffix),
Some("<") => format!("{prefix}{value}{suffix}{padding}"),
Some("=") => format!("{prefix}{padding}{value}{suffix}"),
Some("^") => format!(
"{}{}{}{}{}",
&padding[..padding.len() / 2],
Expand All @@ -590,7 +587,7 @@ impl NumberFormat {
suffix,
&padding[padding.len() / 2..]
),
_ => format!("{}{}{}{}", padding, prefix, value, suffix),
_ => format!("{padding}{prefix}{value}{suffix}"),
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions avenger-scales/src/scalar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,7 @@ impl Scalar {
color.a,
]),
Err(e) => Err(AvengerScaleError::InternalError(format!(
"Scalar string is not a valid color: {}\n{:?}",
color_str, e
"Scalar string is not a valid color: {color_str}\n{e:?}"
))),
}
}
Expand Down Expand Up @@ -514,7 +513,7 @@ impl Scalar {
let arrays: Vec<ArrayRef> = scalars.into_iter().map(|s| s.to_array()).collect();

arrow::compute::concat(&arrays.iter().map(|a| a.as_ref()).collect::<Vec<_>>()).map_err(
|e| AvengerScaleError::InternalError(format!("Failed to concatenate arrays: {}", e)),
|e| AvengerScaleError::InternalError(format!("Failed to concatenate arrays: {e}")),
)
}

Expand All @@ -534,7 +533,7 @@ impl Scalar {
.map(|arr| {
// Convert array to Float32Array and extract values
let cast_arr = arrow::compute::cast(&arr, &DataType::Float32).map_err(|e| {
AvengerScaleError::InternalError(format!("Failed to cast array: {}", e))
AvengerScaleError::InternalError(format!("Failed to cast array: {e}"))
})?;
let float_arr = cast_arr.as_primitive::<Float32Type>();
let values: Vec<Option<f32>> = (0..float_arr.len())
Expand Down
Loading
Loading