use crate::render::{Charset, Color};
use crate::scale::Scale;
use crate::{
Align, Bars, Cells, Document, DocumentKind, Frame, Grid, Line, LineStyle, Plot, PointStyle,
Points, Rule, Text,
};
const V1_PLOT: &str = include_str!("../tests/fixtures/serde/v1/plot.json");
const V1_GRID: &str = include_str!("../tests/fixtures/serde/v1/grid.json");
const LEGACY_PLOT_1_14: &str = include_str!("../tests/fixtures/serde/legacy/plot-1.14.json");
fn frame() -> Frame {
Frame {
charset: Charset::Braille,
..Frame::plain(64, 18)
}
}
fn fixture_plot() -> Plot<'static> {
Plot::new()
.layer(
Line::y(vec![1.0, f64::NAN, 3.0])
.color(Color::Cyan)
.label("series"),
)
.title("v1 plot")
.x_label("step")
.y_label("value")
.y_domain(0.0, 4.0)
}
fn fixture_grid() -> Grid<'static> {
Grid::new(2).with(Plot::new().title("pane"))
}
#[test]
fn a_column_reference_deserializes_inside_with_columns() {
use crate::data::{Series, with_columns};
let json = r#"{"col": 0}"#;
let bound = with_columns(vec![vec![1.0, f64::NAN, 3.0]], || {
serde_json::from_str::<Series>(json).unwrap()
});
assert_eq!(bound.len(), 3);
assert_eq!(bound.as_slice()[0], 1.0);
assert!(bound.as_slice()[1].is_nan());
assert_eq!(bound.as_slice()[2], 3.0);
let error = serde_json::from_str::<Series>(json).unwrap_err();
assert!(error.to_string().contains("not bound"));
}
#[test]
fn a_column_bound_document_renders_like_an_inline_series() {
let inline = Document::plot(crate::line(vec![1.0, 5.0, 2.0, 8.0])).unwrap();
let request = r#"{
"version": 1,
"kind": "plot",
"spec": {
"layers": [{"Line": {"x": null, "y": {"col": 0}, "color": null, "label": null, "style": "Pixels"}}]
}
}"#;
let bound: Document = crate::data::with_columns(vec![vec![1.0, 5.0, 2.0, 8.0]], || {
serde_json::from_str(request).unwrap()
});
let frame = Frame::plain(40, 10);
assert_eq!(bound.render(&frame), inline.render(&frame));
}
#[test]
fn v1_documents_match_their_golden_wire_fixtures() {
let plot = Document::plot(fixture_plot()).unwrap();
assert_eq!(plot.version(), 1);
assert_eq!(plot.kind(), DocumentKind::Plot);
assert!(plot.as_plot().is_some());
assert!(plot.as_grid().is_none());
assert_eq!(
serde_json::to_value(&plot).unwrap(),
serde_json::from_str::<serde_json::Value>(V1_PLOT).unwrap()
);
let grid = Document::grid(fixture_grid()).unwrap();
assert_eq!(grid.kind(), DocumentKind::Grid);
assert!(grid.as_grid().is_some());
assert_eq!(
serde_json::to_value(&grid).unwrap(),
serde_json::from_str::<serde_json::Value>(V1_GRID).unwrap()
);
}
#[test]
fn every_supported_document_fixture_decodes_validates_and_renders() {
let plot: Document = serde_json::from_str(V1_PLOT).unwrap();
assert!(plot.validate().is_ok());
assert_eq!(
plot.try_render(&frame()).unwrap(),
fixture_plot().try_render(&frame()).unwrap()
);
let grid: Document = serde_json::from_str(V1_GRID).unwrap();
assert!(grid.validate().is_ok());
assert!(!grid.render(&frame()).is_empty());
}
#[test]
fn legacy_raw_plot_payloads_remain_decodable() {
let decoded: Plot = serde_json::from_str(LEGACY_PLOT_1_14).unwrap();
assert!(decoded.validate().is_ok());
assert_eq!(decoded.render(&frame()), fixture_plot().render(&frame()));
}
#[test]
fn documents_reject_unknown_versions_and_invalid_specs() {
let mut future: serde_json::Value = serde_json::from_str(V1_PLOT).unwrap();
future["version"] = 2.into();
let error = serde_json::from_value::<Document>(future).unwrap_err();
assert!(
error
.to_string()
.contains("unsupported malevich document version")
);
let mut invalid: serde_json::Value = serde_json::from_str(V1_GRID).unwrap();
invalid["spec"]["columns"] = 0.into();
let error = serde_json::from_value::<Document>(invalid).unwrap_err();
assert!(error.to_string().contains("Grid columns is empty"));
}
#[test]
fn documents_accept_additive_fields_and_default_omitted_plot_fields() {
let mut value: serde_json::Value = serde_json::from_str(V1_PLOT).unwrap();
value["future_envelope_field"] = true.into();
value["spec"]["future_plot_field"] = "ignored".into();
value["spec"].as_object_mut().unwrap().remove("colorbar");
let document: Document = serde_json::from_value(value).unwrap();
assert!(document.validate().is_ok());
assert!(!document.render(&frame()).is_empty());
}
#[test]
fn documents_take_ownership_of_borrowed_series() {
let document = {
let values = [1.0, 2.0, 3.0];
Document::try_from(crate::line(&values[..])).unwrap()
};
assert!(!document.render(&frame()).is_empty());
}
#[test]
fn a_full_spec_round_trips_to_an_identical_render() {
let plot = Plot::new()
.layer(
Line::xy(
&[0.0, 1.0, 2.0, 3.0, 4.0][..],
&[1.0, f64::NAN, 4.0, 2.0, 5.0][..],
)
.label("with a gap")
.color(Color::Rgb(200, 40, 90))
.style(LineStyle::Corners),
)
.layer(
Points::xy(&[0.5, 2.5][..], &[3.0, 1.0][..])
.style(PointStyle::Cross)
.label("crosses"),
)
.layer(Rule::h(2.5))
.layer(Text::at(1.0, 4.5, "note"))
.title("round trip")
.x_label("x")
.y_label("y")
.y_domain(0.0, 6.0);
let encoded = serde_json::to_string(&plot).expect("serializes");
let decoded: Plot = serde_json::from_str(&encoded).expect("deserializes");
assert_eq!(plot.render(&frame()), decoded.render(&frame()));
}
#[test]
fn point_styles_round_trip_and_old_payloads_default_to_dots() {
let points = Points::y(vec![1.0, 2.0]).style(PointStyle::Plus);
let encoded = serde_json::to_string(&points).unwrap();
let decoded: Points<'static> = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.style, PointStyle::Plus);
let legacy = r#"{"x":null,"y":[1.0],"color":null,"label":null}"#;
let decoded: Points<'static> = serde_json::from_str(legacy).unwrap();
assert_eq!(decoded.style, PointStyle::Dot);
}
#[test]
fn text_alignment_round_trips_and_old_payloads_stay_left() {
let text = Text::at(1.0, 2.0, "note").align(Align::Right);
let encoded = serde_json::to_string(&text).unwrap();
let decoded: Text = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.align, Align::Right);
let bare = serde_json::to_string(&Text::at(1.0, 2.0, "note")).unwrap();
assert!(!bare.contains("align"), "{bare}");
let legacy = r#"{"x":1.0,"y":2.0,"text":"note","color":null}"#;
let decoded: Text = serde_json::from_str(legacy).unwrap();
assert_eq!(decoded.align, Align::Left);
}
#[test]
fn gaps_survive_json_as_nulls() {
let plot = Plot::new().layer(Line::y(&[1.0, f64::NAN, 3.0][..]));
let encoded = serde_json::to_string(&plot).expect("serializes");
assert!(encoded.contains("[1.0,null,3.0]"), "{encoded}");
let decoded: Plot = serde_json::from_str(&encoded).expect("deserializes");
assert_eq!(plot.render(&frame()), decoded.render(&frame()));
}
#[test]
fn bands_cells_and_log_scales_round_trip_as_valid_specs() {
let plots = [
Plot::new()
.layer(Bars::new(["a", "b", "c"], &[3.0, 7.0, 5.0][..]))
.x_scale(Scale::bands(["a", "b", "c"])),
Plot::new()
.layer(Cells::matrix(2, &[1.0, 2.0, 3.0, 4.0][..]).extents((1.0, 100.0), (1.0, 1000.0)))
.x_scale(Scale::Log)
.y_scale(Scale::Log),
Plot::new().layer(Cells::rgb(2, vec![(0u8, 0, 0), (255, 255, 255)])),
Plot::new().layer(Cells::classes(2, ["a", "b", "b", "a"])),
Plot::new()
.layer(Cells::matrix(2, &[1.0, 2.0, 3.0, 4.0][..]).reduce(crate::stat::Reducer::Max)),
];
for plot in plots {
assert!(plot.validate().is_ok());
let encoded = serde_json::to_string(&plot).expect("serializes");
let decoded: Plot = serde_json::from_str(&encoded).expect("deserializes");
assert!(decoded.validate().is_ok());
assert_eq!(plot.render(&frame()), decoded.render(&frame()));
}
let matrix = Plot::new().layer(Cells::matrix(1, &[1.0][..]));
let encoded = serde_json::to_string(&matrix).expect("serializes");
assert!(!encoded.contains("rgb"), "spurious field: {encoded}");
assert!(!encoded.contains("classes"), "spurious field: {encoded}");
assert!(!encoded.contains("reduce"), "spurious field: {encoded}");
let max = Plot::new().layer(Cells::matrix(1, &[1.0][..]).reduce(crate::stat::Reducer::Max));
let encoded = serde_json::to_string(&max).expect("serializes");
assert!(encoded.contains("\"reduce\":\"Max\""), "{encoded}");
let decoded: Plot = serde_json::from_str(&encoded).expect("deserializes");
assert!(decoded.validate().is_ok());
let hostile: Plot = serde_json::from_str(
r#"{"layers":[{"Cells":{"columns":1,"values":[1.0],"extents":null,"colormap":{"stops":[[0,0,0],[255,255,255]]},"reduce":{"Percentile":7.0}}}],"title":null,"x":"Linear","y":"Linear","x_label":null,"y_label":null,"x_domain":null,"y_domain":null}"#,
)
.expect("deserializes");
assert!(matches!(
hostile.validate(),
Err(crate::Error::InvalidParameter { .. })
));
}
#[test]
fn a_grid_of_plots_round_trips() {
let grid = Grid::new(2)
.with(crate::line(&[1.0, 3.0, 2.0][..]).title("a"))
.with(crate::line(&[2.0, 1.0, 3.0][..]).title("b"));
let encoded = serde_json::to_string(&grid).expect("serializes");
let decoded: Grid = serde_json::from_str(&encoded).expect("deserializes");
assert_eq!(grid.render(&frame()), decoded.render(&frame()));
}
#[test]
fn color_by_and_the_palette_round_trip_and_stay_out_of_legacy_encodings() {
use crate::mark::Points;
use crate::scale::Palette;
let plot = Plot::new()
.layer(Points::y(&[1.0, 2.0, 3.0][..]).color_by(["a", "b", "a"]))
.palette(Palette::new(&[Color::Red, Color::Blue]));
let encoded = serde_json::to_string(&plot).expect("serializes");
assert!(encoded.contains("color_by") && encoded.contains("palette"));
let decoded: Plot = serde_json::from_str(&encoded).expect("deserializes");
assert_eq!(plot.render(&frame()), decoded.render(&frame()));
let legacy = Plot::new().layer(Points::y(&[1.0, 2.0][..]));
let legacy_encoded = serde_json::to_string(&legacy).expect("serializes");
assert!(!legacy_encoded.contains("color_by") && !legacy_encoded.contains("palette"));
let ragged: Plot = serde_json::from_str(
r#"{"layers":[{"Points":{"x":null,"y":[1.0,2.0,3.0],"color":null,"label":null,"style":"Dot","color_by":["a"]}}],"title":null,"x":"Linear","y":"Linear","x_label":null,"y_label":null,"x_domain":null,"y_domain":null}"#,
)
.expect("ragged color_by deserializes");
assert!(matches!(
ragged.validate(),
Err(crate::Error::UnequalChannels { .. })
));
}
#[test]
fn a_centered_colormap_round_trips_and_legacy_maps_stay_linear() {
use crate::scale::Colormap;
let centered = Colormap::RED_BLUE.centered_at(0.0);
let encoded = serde_json::to_string(¢ered).expect("serializes");
assert!(encoded.contains("midpoint"), "midpoint missing: {encoded}");
let decoded: Colormap = serde_json::from_str(&encoded).expect("deserializes");
assert_eq!(decoded, centered);
let linear = serde_json::to_string(&Colormap::GREYS).expect("serializes");
assert!(!linear.contains("midpoint"), "spurious field: {linear}");
let legacy: Colormap =
serde_json::from_str(r#"{"stops":[[0,0,0],[255,255,255]]}"#).expect("deserializes");
assert_eq!(legacy.midpoint(), None);
assert_eq!(legacy.position_in(1.0, 0.0, 4.0), 0.25);
let log = Colormap::MAGMA.log();
let encoded = serde_json::to_string(&log).expect("serializes");
assert!(encoded.contains("\"log\":true"), "log missing: {encoded}");
let decoded: Colormap = serde_json::from_str(&encoded).expect("deserializes");
assert_eq!(decoded, log);
assert!(!linear.contains("log"), "spurious log field: {linear}");
let contradictory: Colormap =
serde_json::from_str(r#"{"stops":[[0,0,0],[255,255,255]],"midpoint":0.0,"log":true}"#)
.expect("deserializes");
assert!(matches!(
contradictory.validate(),
Err(crate::Error::InvalidParameter { .. })
));
}
#[test]
fn malformed_payloads_render_without_panicking() {
let colormap: crate::scale::Colormap =
serde_json::from_str(r#"{"stops":[]}"#).expect("empty colormap deserializes");
assert_eq!(colormap.color(0.5), Color::Default);
let grid: Grid = serde_json::from_str(
r#"{"columns":0,"plots":[{"layers":[],"title":null,"x":"Linear","y":"Linear","x_label":null,"y_label":null,"x_domain":null,"y_domain":null}]}"#,
)
.expect("zero-column grid deserializes");
assert!(matches!(
grid.validate(),
Err(crate::Error::EmptyDimension { .. })
));
assert!(grid.try_render(&frame()).is_err());
assert_eq!(grid.render(&frame()), "");
let ragged = r#"{"layers":[{"Range":{"placement":{"Numeric":[0.0,1.0,2.0]},"low":[0.0],"high":[5.0,6.0],"body":null,"marker":[1.0,2.0,3.0,4.0],"color":null,"label":null}}],"title":null,"x":"Linear","y":"Linear","x_label":null,"y_label":null,"x_domain":null,"y_domain":null}"#;
let plot: Plot = serde_json::from_str(ragged).expect("ragged range deserializes");
let _ = plot.render(&frame());
let ragged_line: Plot = serde_json::from_value(serde_json::json!({
"layers": [{
"Line": {
"x": [0.0],
"y": vec![1.0; 1_000],
"color": null,
"label": null,
"style": "Pixels"
}
}]
}))
.expect("ragged large line deserializes");
assert!(matches!(
ragged_line.validate(),
Err(crate::Error::UnequalChannels { .. })
));
let _ = ragged_line.render(&Frame::plain(20, 8));
}
#[test]
fn validate_rejects_the_malformed_payloads_render_tolerates() {
let ragged: Plot = serde_json::from_str(
r#"{"layers":[{"Cells":{"columns":0,"values":[1.0,2.0,3.0],"extents":null,"colormap":{"stops":[[0,0,0],[255,255,255]]}}}],"title":null,"x":"Linear","y":"Linear","x_label":null,"y_label":null,"x_domain":null,"y_domain":null}"#,
)
.expect("zero-column cells deserializes");
assert!(matches!(
ragged.validate(),
Err(crate::Error::EmptyDimension { .. })
));
assert!(ragged.try_render(&frame()).is_err());
let _ = ragged.render(&frame());
let degenerate: Plot = serde_json::from_str(
r#"{"layers":[{"Cells":{"columns":1,"values":[1.0],"extents":[[2.0,2.0],[0.0,1.0]],"colormap":{"stops":[[0,0,0],[255,255,255]]}}}],"title":null,"x":"Linear","y":"Linear","x_label":null,"y_label":null,"x_domain":null,"y_domain":null}"#,
)
.expect("degenerate Cells extents deserialize");
assert!(matches!(
degenerate.validate(),
Err(crate::Error::InvalidParameter { .. })
));
let categorical_y: Plot = serde_json::from_str(
r#"{"layers":[{"Cells":{"columns":1,"values":[1.0],"extents":null,"colormap":{"stops":[[0,0,0],[255,255,255]]}}}],"title":null,"x":"Linear","y":{"Bands":["a","b"]},"x_label":null,"y_label":null,"x_domain":null,"y_domain":null}"#,
)
.expect("banded y scale deserializes");
assert!(matches!(
categorical_y.validate(),
Err(crate::Error::IncompatibleScale { .. })
));
}
#[test]
fn strict_validation_reuses_constructor_invariants() {
let valid = Plot::new().layer(Bars::spans(0.0, 1.0, [1.0]));
let mut raw = serde_json::to_value(valid).expect("plot serializes");
*raw.pointer_mut("/layers/0/Bars/placement/Spans/width")
.expect("span width in wire representation") = serde_json::json!(0.0);
let invalid: Plot = serde_json::from_value(raw.clone()).expect("raw Plot stays decodable");
assert!(matches!(
invalid.validate(),
Err(crate::Error::InvalidParameter { .. })
));
let _ = invalid.render(&frame());
let envelope = serde_json::json!({
"version": Document::VERSION,
"kind": "plot",
"spec": raw
});
let error = serde_json::from_value::<Document>(envelope)
.expect_err("a strict Document rejects invalid Bars geometry");
assert!(
error.to_string().contains("finite positive width"),
"{error}"
);
let mut reversed = serde_json::to_value(Plot::new()).unwrap();
reversed["x_domain"] = serde_json::json!([2.0, 1.0]);
let reversed: Plot = serde_json::from_value(reversed).unwrap();
assert!(matches!(
reversed.validate(),
Err(crate::Error::InvalidParameter { .. })
));
let mut empty_palette =
serde_json::to_value(Plot::new().palette(crate::scale::Palette::default())).unwrap();
empty_palette["palette"]["colors"] = serde_json::json!([]);
let empty_palette: Plot = serde_json::from_value(empty_palette).unwrap();
assert!(matches!(
empty_palette.validate(),
Err(crate::Error::EmptyDimension { .. })
));
}
#[test]
fn a_function_line_refuses_to_serialize() {
let plot = Plot::new().layer(Line::function(0.0..10.0, f64::sin));
let error = serde_json::to_string(&plot).expect_err("closures have no data form");
assert!(
error.to_string().contains("sample it into points"),
"{error}"
);
let document = Document::plot(Plot::new().layer(Line::function(0.0..10.0, f64::sin))).unwrap();
let error = serde_json::to_string(&document).expect_err("the envelope cannot encode a closure");
assert!(error.to_string().contains("sample it into points"));
}
#[test]
fn positioned_and_based_bars_round_trip_and_stay_off_the_old_wire() {
let stacked = Plot::new()
.layer(Bars::new(["a", "b"], &[1.0, 2.0][..]))
.layer(Bars::new(["a", "b"], &[2.0, 1.0][..]).base(&[1.0, 2.0][..]));
let grouped = Plot::new()
.x_scale(Scale::bands(["a", "b"]))
.layer(Bars::at(&[-0.2, 0.8][..], 0.35, &[1.0, 2.0][..]))
.layer(Bars::at(&[0.2, 1.2][..], 0.35, &[2.0, 1.0][..]));
for plot in [stacked, grouped] {
assert!(plot.validate().is_ok());
let encoded = serde_json::to_string(&plot).expect("serializes");
let decoded: Plot = serde_json::from_str(&encoded).expect("deserializes");
assert!(decoded.validate().is_ok());
assert_eq!(plot.render(&frame()), decoded.render(&frame()));
}
let plain = Plot::new().layer(Bars::new(["a"], &[1.0][..]));
let encoded = serde_json::to_string(&plain).expect("serializes");
assert!(!encoded.contains("base"), "spurious field: {encoded}");
let based = Plot::new().layer(Bars::new(["a", "b"], &[1.0, 2.0][..]).base(&[0.0, 1.0][..]));
let mut raw: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&based).unwrap()).unwrap();
*raw.pointer_mut("/layers/0/Bars/base")
.expect("the base field is on the wire") = serde_json::json!([0.0]);
let decoded: Plot = serde_json::from_value(raw).expect("decodes structurally");
assert!(
decoded.validate().is_err(),
"a ragged base must fail validation"
);
}
#[test]
fn a_viewport_persists_its_windows_and_nothing_derived() {
let stamps = [1.0, 10.0, 100.0, 1000.0];
let plot = Plot::new()
.layer(Line::xy(&stamps[..], &stamps[..]))
.x_scale(Scale::Log);
let seeded = plot.mapping(&frame()).viewport();
assert!(seeded.x().is_some(), "a log axis seeds a window");
let encoded = serde_json::to_string(&seeded).expect("serializes");
assert!(
!encoded.contains("log"),
"derived state on the wire: {encoded}"
);
let decoded: crate::Viewport = serde_json::from_str(&encoded).expect("deserializes");
assert_eq!(decoded.x(), seeded.x());
assert_eq!(decoded.y(), seeded.y());
}
#[test]
fn horizontal_bars_round_trip_and_stay_out_of_vertical_encodings() {
let plot = Plot::new().layer(Bars::new(["a", "b"], &[1.0, 2.0][..]).horizontal());
let document = Document::plot(plot.clone()).expect("a horizontal bars plot is valid");
let json = serde_json::to_string(&document).expect("serializes");
assert!(json.contains("\"horizontal\":true"), "{json}");
let decoded: Document = serde_json::from_str(&json).expect("decodes");
assert_eq!(
decoded.try_render(&frame()).expect("renders"),
plot.render(&frame())
);
let vertical =
serde_json::to_string(&Document::plot(crate::bar(["a"], &[1.0][..])).unwrap()).unwrap();
assert!(!vertical.contains("horizontal"), "{vertical}");
}
#[test]
fn rule_spans_round_trip_and_stay_off_the_line_wire() {
let plot = Plot::new()
.layer(Rule::v_span(1.0, 3.0).label("warmup"))
.layer(Rule::h_span(0.5, 0.25))
.layer(Rule::h(2.5));
let json = serde_json::to_string(&plot).unwrap();
assert!(json.contains("\"VerticalSpan\":[1.0,3.0]"), "{json}");
assert!(json.contains("\"HorizontalSpan\":[0.5,0.25]"), "{json}");
assert!(json.contains("\"Horizontal\":2.5"), "{json}");
let back: Plot<'static> = serde_json::from_str(&json).unwrap();
assert_eq!(back.render(&frame()), plot.render(&frame()));
let line = serde_json::to_string(&Plot::new().layer(Rule::h(2.5))).unwrap();
assert!(!line.contains("Span"), "{line}");
}
#[test]
fn axes_off_round_trips_and_stays_off_the_wire_when_shown() {
let shown = serde_json::to_string(&Plot::new().layer(Line::y(&[1.0, 2.0][..]))).unwrap();
assert!(!shown.contains("axes"), "{shown}");
let bare = Plot::new().layer(Line::y(&[1.0, 2.0][..])).axes(false);
let json = serde_json::to_string(&bare).unwrap();
assert!(json.contains("\"axes\":false"), "{json}");
let back: Plot<'static> = serde_json::from_str(&json).unwrap();
assert_eq!(back.render(&frame()), bare.render(&frame()));
}
#[test]
fn units_and_integer_axes_round_trip_and_stay_off_the_plain_wire() {
use crate::scale::{Scale, Unit};
let plain = serde_json::to_string(&Plot::new().layer(Line::y(&[1.0, 2.0][..]))).unwrap();
assert!(!plain.contains("unit"), "{plain}");
let plot = Plot::new()
.layer(Line::y(&[1.0, 2.0][..]))
.y_unit(Unit::si("B"))
.x_unit(Unit::Bytes)
.y_scale(Scale::Integer);
let json = serde_json::to_string(&plot).unwrap();
assert!(json.contains("\"y_unit\":{\"Si\":\"B\"}"), "{json}");
assert!(json.contains("\"x_unit\":\"Bytes\""), "{json}");
assert!(json.contains("\"y\":\"Integer\""), "{json}");
let back: Plot<'static> = serde_json::from_str(&json).unwrap();
assert_eq!(back.render(&frame()), plot.render(&frame()));
}
#[test]
fn one_sided_domains_write_their_end_and_pairs_stay_pairs() {
let floored = Plot::new().layer(Line::y(vec![1.0, 2.0])).y_min(0.0);
let json = serde_json::to_value(&floored).unwrap();
assert_eq!(json["y_domain"], serde_json::json!({"min": 0.0}));
assert_eq!(json["x_domain"], serde_json::Value::Null);
let capped = Plot::new().layer(Line::y(vec![1.0, 2.0])).x_max(5.0);
assert_eq!(
serde_json::to_value(&capped).unwrap()["x_domain"],
serde_json::json!({"max": 5.0})
);
let both = Plot::new().y_min(0.0).y_max(4.0);
assert_eq!(
serde_json::to_value(&both).unwrap()["y_domain"],
serde_json::json!([0.0, 4.0])
);
assert_eq!(
serde_json::to_string(&both).unwrap(),
serde_json::to_string(&Plot::new().y_domain(0.0, 4.0)).unwrap()
);
let frame = Frame::plain(40, 10);
let back: Plot = serde_json::from_str(&serde_json::to_string(&floored).unwrap()).unwrap();
assert_eq!(back.render(&frame), floored.render(&frame));
assert_eq!(back.mapping(&frame).y_domain().0, 0.0);
let mut sided = serde_json::to_value(Plot::new()).unwrap();
sided["x_domain"] = serde_json::json!({"min": 1.0, "max": 2.0});
let sided: Plot = serde_json::from_value(sided).unwrap();
assert_eq!(
serde_json::to_value(&sided).unwrap()["x_domain"],
serde_json::json!([1.0, 2.0])
);
let mut empty = serde_json::to_value(Plot::new()).unwrap();
empty["x_domain"] = serde_json::json!({});
let error = serde_json::from_value::<Plot>(empty).expect_err("an empty domain object");
assert!(error.to_string().contains("min"), "{error}");
}
#[test]
fn colormap_options_stay_off_the_plain_wire_and_round_trip() {
use crate::scale::Colormap;
let plain = serde_json::to_value(Colormap::GREYS).unwrap();
assert_eq!(
plain,
serde_json::json!({"stops": [[64, 64, 64], [250, 250, 250]]})
);
let configured = Colormap::GREYS
.domain(0.0, 1.0)
.under(Color::Blue)
.over(Color::Red)
.steps(3);
let json = serde_json::to_value(&configured).unwrap();
assert_eq!(json["domain"], serde_json::json!([0.0, 1.0]));
assert_eq!(json["steps"], serde_json::json!(3));
let back: Colormap = serde_json::from_value(json).unwrap();
assert_eq!(back, configured);
let split = Colormap::GREYS.thresholds([1.0, 2.0]);
let json = serde_json::to_value(&split).unwrap();
assert_eq!(json["thresholds"], serde_json::json!([1.0, 2.0]));
assert_eq!(serde_json::from_value::<Colormap>(json).unwrap(), split);
let mut unsorted = serde_json::to_value(&split).unwrap();
unsorted["thresholds"] = serde_json::json!([2.0, 1.0]);
let unsorted: Colormap = serde_json::from_value(unsorted).unwrap();
assert!(unsorted.validate().is_err());
}
#[test]
fn interval_bars_round_trip() {
let plot = Plot::new().layer(Bars::intervals(
vec![0.0, 1.0],
vec![1.0, 3.5],
vec![2.0, 1.0],
));
let json = serde_json::to_value(&plot).unwrap();
assert_eq!(
json["layers"][0]["Bars"]["placement"]["Intervals"]["ends"],
serde_json::json!([1.0, 3.5])
);
let back: Plot = serde_json::from_value(json).unwrap();
let frame = Frame::plain(40, 10);
assert_eq!(back.render(&frame), plot.render(&frame));
let mut reversed = serde_json::to_value(&plot).unwrap();
reversed["layers"][0]["Bars"]["placement"]["Intervals"]["ends"] = serde_json::json!([1.0, 0.5]);
let reversed: Plot = serde_json::from_value(reversed).unwrap();
assert!(reversed.validate().is_err());
}