use super::*;
const NUMERIC: &str = r#"{"schemaVersion": 1, "type": "heatmap", "title": "Load", "width": 600, "height": 360,
"heatmap": {"rows": ["Mon", "Tue"], "columns": ["AM", "PM", "Night"], "valueLabel": "Tickets",
"cells": [
{"row": "Mon", "column": "AM", "value": 10}, {"row": "Mon", "column": "PM", "value": 50},
{"row": "Mon", "column": "Night", "value": 90}, {"row": "Tue", "column": "AM", "value": 30},
{"row": "Tue", "column": "PM", "value": 70}
]}}"#;
const CATEGORICAL: &str = r#"{"schemaVersion": 1, "type": "heatmap", "title": "Access", "width": 600, "height": 360,
"heatmap": {"rows": ["Reader", "Owner"], "columns": ["Pages", "Settings"],
"categories": ["Read", "Write", "None"],
"cells": [
{"row": "Reader", "column": "Pages", "category": "Read"},
{"row": "Reader", "column": "Settings", "category": "None"},
{"row": "Owner", "column": "Pages", "category": "Write"},
{"row": "Owner", "column": "Settings", "category": "Write"}
]}}"#;
fn html(spec: &str) -> String {
render_json(spec, RenderFormat::Html, &RenderOptions::default())
.unwrap()
.content
}
#[test]
fn a_numeric_heatmap_colors_values_writes_them_and_marks_the_missing_cell() {
let output = render_ok(NUMERIC);
assert!(output.warnings.is_empty(), "{:?}", output.warnings);
let svg = &output.content;
assert_eq!(
svg.matches("class=\"chartlet-hm-cell chartlet-hm-s")
.count(),
5
);
assert_eq!(
svg.matches("class=\"chartlet-hm-key chartlet-hm-s").count(),
6
);
assert_eq!(svg.matches("class=\"chartlet-hm-missing\"").count(), 2);
assert!(svg.contains("<title>Tue, Night: no value</title>"));
assert!(svg.contains("<title>Mon, Night: 90</title>"));
assert!(svg.contains("chartlet-hm-cell chartlet-hm-s0\""));
assert!(svg.contains("chartlet-hm-cell chartlet-hm-s5\""));
assert!(svg.contains("class=\"chartlet-hm-value chartlet-hm-on-light\""));
assert!(svg.contains("class=\"chartlet-hm-value chartlet-hm-on-dark\""));
assert!(svg.contains(">Tickets</text>"));
}
#[test]
fn the_table_and_the_description_hold_every_cell() {
let html = html(NUMERIC);
assert!(html.contains(
"<th scope=\"col\">Row</th><th scope=\"col\">Column</th><th scope=\"col\">Tickets</th>"
));
assert!(html.contains("<th scope=\"row\">Mon</th><td>Night</td><td>90</td>"));
assert!(html.contains("<th scope=\"row\">Tue</th><td>Night</td><td>no value</td>"));
assert!(html.contains("Heatmap of 2 rows by 3 columns. Color shows Tickets, from 10 (lowest: Mon, AM (10)) to 90 (highest: Mon, Night (90)). 1 without a value."));
}
#[test]
fn categories_have_a_fill_and_a_symbol_each() {
let output = render_ok(CATEGORICAL);
assert!(output.warnings.is_empty(), "{:?}", output.warnings);
let svg = &output.content;
for (class, cells) in [("c1", 1), ("c2", 2), ("c3", 1)] {
assert_eq!(
svg.matches(&format!("class=\"chartlet-hm-cell chartlet-hm-{class}\""))
.count(),
cells,
"{class}"
);
}
assert_eq!(svg.matches("class=\"chartlet-hm-symbol\"").count(), 4 + 3);
assert!(svg.contains("<circle"));
assert!(svg.contains("<polyline"));
let html = html(CATEGORICAL);
assert!(html.contains("<th scope=\"col\">Category</th>"));
assert!(html.contains("<th scope=\"row\">Reader</th><td>Settings</td><td>None</td>"));
assert!(html.contains("Cells by category: Read 1, Write 2, None 1."));
}
#[test]
fn a_diverging_scale_parts_at_the_midpoint() {
let json = NUMERIC.replace(
"\"valueLabel\"",
"\"scale\": \"diverging\", \"midpoint\": 50, \"valueLabel\"",
);
let svg = render_ok(&json).content;
assert_eq!(
svg.matches("class=\"chartlet-hm-key chartlet-hm-d").count(),
7
);
assert!(svg.contains("<title>Mon, PM: 50</title>"));
for (cell, step) in [("Mon, AM", "d0"), ("Mon, PM", "d3"), ("Mon, Night", "d6")] {
let at = svg
.find(&format!("<title>{cell}:"))
.expect("the cell is drawn");
let rect = &svg[svg[..at].rfind("<rect").unwrap()..at];
assert!(
rect.contains(&format!("chartlet-hm-{step}\"")),
"{cell}: {rect}"
);
}
}
#[test]
fn rows_and_columns_follow_the_order_asked_for() {
let positions = |svg: &str, labels: &[&str]| -> Vec<usize> {
let mut found: Vec<(usize, &str)> = labels
.iter()
.map(|label| {
(
svg.find(&format!(">{label}</text>")).expect("label"),
*label,
)
})
.collect();
found.sort_unstable();
found
.iter()
.map(|(_, label)| labels.iter().position(|l| l == label).unwrap())
.collect()
};
let listed = render_ok(NUMERIC).content;
assert_eq!(positions(&listed, &["Mon", "Tue"]), [0, 1]);
let by_value = render_ok(&NUMERIC.replace(
"\"valueLabel\"",
"\"rowOrder\": \"value\", \"columnOrder\": \"value\", \"valueLabel\"",
))
.content;
assert_eq!(positions(&by_value, &["Night", "PM", "AM"]), [0, 1, 2]);
let by_label = render_ok(&NUMERIC.replace(
"\"valueLabel\"",
"\"columnOrder\": \"label\", \"valueLabel\"",
))
.content;
assert_eq!(positions(&by_label, &["AM", "Night", "PM"]), [0, 1, 2]);
}
#[test]
fn heatmaps_are_validated_by_name() {
for (spec, from, to, code, path) in [
(
NUMERIC,
"\"row\": \"Mon\", \"column\": \"AM\"",
"\"row\": \"Sun\", \"column\": \"AM\"",
"unknown_row",
"/heatmap/cells/0/row",
),
(
NUMERIC,
"\"column\": \"AM\", \"value\": 10",
"\"column\": \"Dawn\", \"value\": 10",
"unknown_column",
"/heatmap/cells/0/column",
),
(
NUMERIC,
"\"column\": \"PM\", \"value\": 50",
"\"column\": \"AM\", \"value\": 50",
"duplicate_cell",
"/heatmap/cells/1",
),
(
NUMERIC,
"\"columns\": [\"AM\", \"PM\"",
"\"columns\": [\"AM\", \"AM\"",
"duplicate_label",
"/heatmap/columns/1",
),
(
NUMERIC,
"\"value\": 10}",
"\"category\": \"Read\"}",
"invalid_cell",
"/heatmap/cells/0",
),
(
NUMERIC,
"\"valueLabel\"",
"\"midpoint\": 5, \"valueLabel\"",
"option_not_supported",
"/heatmap/midpoint",
),
(
CATEGORICAL,
"\"category\": \"Read\"",
"\"category\": \"Admin\"",
"unknown_category",
"/heatmap/cells/0/category",
),
(
CATEGORICAL,
"\"categories\"",
"\"rowOrder\": \"value\", \"categories\"",
"invalid_order",
"/heatmap/rowOrder",
),
(
CATEGORICAL,
"\"categories\": [\"Read\", \"Write\", \"None\"]",
"\"categories\": [\"A\", \"B\", \"C\", \"D\", \"E\"]",
"too_many_series",
"/heatmap/categories",
),
] {
assert_eq!(
render_err(&spec.replace(from, to)),
(code, path.to_owned()),
"{from}"
);
}
}