Skip to content

Commit dc3848f

Browse files
authored
fix: strip disallowed properties from secondary encoding channels (#169)
1 parent 84fa30f commit dc3848f

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

src/writer/vegalite/mod.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,17 @@ fn build_layer_encoding(
271271
channel_name = "fillOpacity".to_string();
272272
}
273273

274+
// Secondary positional channels (x2, y2, theta2, radius2) only support
275+
// field/datum/value in Vega-Lite — not type, scale, axis, or title
276+
if matches!(channel_name.as_str(), "x2" | "y2" | "theta2" | "radius2") {
277+
let secondary_encoding = match value {
278+
AestheticValue::Column { name: col, .. } => json!({"field": col}),
279+
AestheticValue::Literal(lit) => json!({"value": lit.to_json()}),
280+
};
281+
encoding.insert(channel_name, secondary_encoding);
282+
continue;
283+
}
284+
274285
let channel_encoding = build_encoding_channel(aesthetic, value, &mut enc_ctx)?;
275286
encoding.insert(channel_name, channel_encoding);
276287

@@ -2345,4 +2356,69 @@ mod tests {
23452356
"x encoding SHOULD have domain when using fixed scales"
23462357
);
23472358
}
2359+
2360+
#[test]
2361+
fn test_secondary_channels_have_no_disallowed_properties() {
2362+
// Vega-Lite secondary channels (x2, y2, theta2, radius2) only support:
2363+
// field, aggregate, bandPosition, bin, timeUnit, title, value.
2364+
// Properties like type, scale, and axis must NOT be emitted.
2365+
let writer = VegaLiteWriter::new();
2366+
2367+
// Segment geom requires pos1end and pos2end as column mappings,
2368+
// which map to x2 and y2 in Vega-Lite.
2369+
let mut spec = Plot::new();
2370+
let layer = Layer::new(Geom::segment())
2371+
.with_aesthetic(
2372+
"x".to_string(),
2373+
AestheticValue::standard_column("x1".to_string()),
2374+
)
2375+
.with_aesthetic(
2376+
"y".to_string(),
2377+
AestheticValue::standard_column("y1".to_string()),
2378+
)
2379+
.with_aesthetic(
2380+
"xend".to_string(),
2381+
AestheticValue::standard_column("x2".to_string()),
2382+
)
2383+
.with_aesthetic(
2384+
"yend".to_string(),
2385+
AestheticValue::standard_column("y2".to_string()),
2386+
);
2387+
spec.layers.push(layer);
2388+
2389+
let df = df! {
2390+
"x1" => &[0, 1],
2391+
"y1" => &[0, 1],
2392+
"x2" => &[1, 2],
2393+
"y2" => &[1, 2],
2394+
}
2395+
.unwrap();
2396+
2397+
transform_spec(&mut spec);
2398+
let json_str = writer.write(&spec, &wrap_data(df)).unwrap();
2399+
let vl_spec: Value = serde_json::from_str(&json_str).unwrap();
2400+
2401+
for channel in ["x2", "y2"] {
2402+
for layer in vl_spec["layer"].as_array().unwrap() {
2403+
if let Some(enc) = layer.get("encoding").and_then(|e| e.get(channel)) {
2404+
assert!(
2405+
enc.get("field").is_some(),
2406+
"{channel} should have 'field': {enc}"
2407+
);
2408+
assert!(
2409+
enc.get("type").is_none(),
2410+
"{channel} should not have 'type': {enc}"
2411+
);
2412+
assert!(
2413+
enc.get("scale").is_none(),
2414+
"{channel} should not have 'scale': {enc}"
2415+
);
2416+
assert!(
2417+
enc.get("axis").is_none(),
2418+
"{channel} should not have 'axis': {enc}"
2419+
);
2420+
}
2421+
}
2422+
}
2423+
}
23482424
}

0 commit comments

Comments
 (0)