use uzor_figures::scale::color::CategoricalScale;
use uzor_figures::theme::FigureTheme;
pub const DEFAULT_CATEGORY_PALETTE: [&str; 10] =
["#4d90fe", "#e0703c", "#5cb87a", "#c94f7c", "#d9b64e", "#7e6bd9", "#3fb6c9", "#e0555a", "#8fbf5f", "#c78bd9"];
pub fn default_category_palette() -> CategoricalScale {
CategoricalScale::new(DEFAULT_CATEGORY_PALETTE.iter().map(|&s| s.to_owned()).collect())
}
#[derive(Debug, Clone)]
pub struct GraphTheme {
pub selection_ring_color: String,
pub selection_ring_width: f64,
pub selection_ring_offset_px: f64,
pub hover_ring_color: String,
pub hover_ring_width: f64,
pub hover_ring_offset_px: f64,
pub label_fill: String,
pub label_font: String,
pub label_offset_x: f64,
pub label_offset_y: f64,
pub cluster_accent: String,
pub cluster_ring_inner_offset_px: f64,
pub cluster_ring_outer_offset_px: f64,
pub cluster_ring_width: f64,
pub cluster_label_color: String,
pub cluster_label_offset_x: f64,
pub cluster_label_offset_y: f64,
pub cluster_edge_width_cap: f64,
pub dim_node_fill: String,
pub dim_alpha: f64,
pub edge_color: String,
pub edge_width: f64,
pub edge_dim_color: String,
pub edge_dim_width: f64,
pub box_select_fill: String,
pub box_select_fill_alpha: f64,
pub box_select_border: String,
pub box_select_border_width: f64,
pub hover_card: FigureTheme,
pub category_palette: CategoricalScale,
pub label_offset_3d_x: f64,
pub label_offset_3d_y: f64,
pub cluster_label_extra_offset_3d_x: f64,
pub grid_tick_font: String,
pub grid_tick_color: String,
}
impl GraphTheme {
pub fn dark() -> Self {
Self {
selection_ring_color: "#ffffff".to_owned(),
selection_ring_width: 2.0,
selection_ring_offset_px: 2.0,
hover_ring_color: "#ffd76a".to_owned(),
hover_ring_width: 1.5,
hover_ring_offset_px: 1.5,
label_fill: "#e6e6ea".to_owned(),
label_font: "11px sans-serif".to_owned(),
label_offset_x: 4.0,
label_offset_y: 4.0,
cluster_accent: "#c9a94e".to_owned(),
cluster_ring_inner_offset_px: 3.0,
cluster_ring_outer_offset_px: 7.0,
cluster_ring_width: 2.0,
cluster_label_color: "#f0e6c0".to_owned(),
cluster_label_offset_x: 10.0,
cluster_label_offset_y: 4.0,
cluster_edge_width_cap: 6.0,
dim_node_fill: "#6b7280".to_owned(),
dim_alpha: 0.15,
edge_color: "#7c8496".to_owned(),
edge_width: 1.7,
edge_dim_color: "#5a6070".to_owned(),
edge_dim_width: 1.3,
box_select_fill: "#4d90fe".to_owned(),
box_select_fill_alpha: 0.15,
box_select_border: "#7fb2ff".to_owned(),
box_select_border_width: 1.0,
hover_card: FigureTheme::dark(),
category_palette: default_category_palette(),
label_offset_3d_x: 6.0,
label_offset_3d_y: 4.0,
cluster_label_extra_offset_3d_x: 10.0,
grid_tick_font: "10px sans-serif".to_owned(),
grid_tick_color: "#8a93a6".to_owned(),
}
}
pub fn light() -> Self {
Self {
selection_ring_color: "#1a1a2e".to_owned(),
selection_ring_width: 2.0,
selection_ring_offset_px: 2.0,
hover_ring_color: "#b8790f".to_owned(),
hover_ring_width: 1.5,
hover_ring_offset_px: 1.5,
label_fill: "#1f2430".to_owned(),
label_font: "11px sans-serif".to_owned(),
label_offset_x: 4.0,
label_offset_y: 4.0,
cluster_accent: "#8a6d1f".to_owned(),
cluster_ring_inner_offset_px: 3.0,
cluster_ring_outer_offset_px: 7.0,
cluster_ring_width: 2.0,
cluster_label_color: "#4a3d10".to_owned(),
cluster_label_offset_x: 10.0,
cluster_label_offset_y: 4.0,
cluster_edge_width_cap: 6.0,
dim_node_fill: "#9aa0ac".to_owned(),
dim_alpha: 0.15,
edge_color: "#5c6579".to_owned(),
edge_width: 1.7,
edge_dim_color: "#c7ccd6".to_owned(),
edge_dim_width: 1.3,
box_select_fill: "#4d90fe".to_owned(),
box_select_fill_alpha: 0.15,
box_select_border: "#2f6fd1".to_owned(),
box_select_border_width: 1.0,
hover_card: FigureTheme::light(),
category_palette: default_category_palette(),
label_offset_3d_x: 6.0,
label_offset_3d_y: 4.0,
cluster_label_extra_offset_3d_x: 10.0,
grid_tick_font: "10px sans-serif".to_owned(),
grid_tick_color: "#5c6579".to_owned(),
}
}
pub fn high_contrast() -> Self {
Self {
selection_ring_color: "#ffff00".to_owned(),
selection_ring_width: 2.0,
selection_ring_offset_px: 2.0,
hover_ring_color: "#00e5ff".to_owned(),
hover_ring_width: 1.5,
hover_ring_offset_px: 1.5,
label_fill: "#ffffff".to_owned(),
label_font: "11px sans-serif".to_owned(),
label_offset_x: 4.0,
label_offset_y: 4.0,
cluster_accent: "#ff9d00".to_owned(),
cluster_ring_inner_offset_px: 3.0,
cluster_ring_outer_offset_px: 7.0,
cluster_ring_width: 2.0,
cluster_label_color: "#ffffff".to_owned(),
cluster_label_offset_x: 10.0,
cluster_label_offset_y: 4.0,
cluster_edge_width_cap: 6.0,
dim_node_fill: "#4d4d4d".to_owned(),
dim_alpha: 0.15,
edge_color: "#ffffff".to_owned(),
edge_width: 1.7,
edge_dim_color: "#808080".to_owned(),
edge_dim_width: 1.3,
box_select_fill: "#00aaff".to_owned(),
box_select_fill_alpha: 0.25,
box_select_border: "#00eaff".to_owned(),
box_select_border_width: 1.5,
hover_card: FigureTheme::high_contrast(),
category_palette: default_category_palette(),
label_offset_3d_x: 6.0,
label_offset_3d_y: 4.0,
cluster_label_extra_offset_3d_x: 10.0,
grid_tick_font: "10px sans-serif".to_owned(),
grid_tick_color: "#cccccc".to_owned(),
}
}
}
impl Default for GraphTheme {
fn default() -> Self {
Self::dark()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dark_matches_the_pre_existing_hardcoded_literals() {
let theme = GraphTheme::dark();
assert_eq!(theme.selection_ring_color, "#ffffff");
assert_eq!(theme.selection_ring_width, 2.0);
assert_eq!(theme.selection_ring_offset_px, 2.0);
assert_eq!(theme.hover_ring_color, "#ffd76a");
assert_eq!(theme.hover_ring_width, 1.5);
assert_eq!(theme.hover_ring_offset_px, 1.5);
assert_eq!(theme.label_fill, "#e6e6ea");
assert_eq!(theme.label_font, "11px sans-serif");
assert_eq!(theme.label_offset_x, 4.0);
assert_eq!(theme.label_offset_y, 4.0);
assert_eq!(theme.cluster_accent, "#c9a94e");
assert_eq!(theme.cluster_ring_inner_offset_px, 3.0);
assert_eq!(theme.cluster_ring_outer_offset_px, 7.0);
assert_eq!(theme.cluster_ring_width, 2.0);
assert_eq!(theme.cluster_label_color, "#f0e6c0");
assert_eq!(theme.cluster_label_offset_x, 10.0);
assert_eq!(theme.cluster_label_offset_y, 4.0);
assert_eq!(theme.cluster_edge_width_cap, 6.0);
assert_eq!(theme.dim_node_fill, "#6b7280");
assert_eq!(theme.dim_alpha, 0.15);
assert_eq!(theme.edge_color, "#7c8496");
assert_eq!(theme.edge_width, 1.7);
assert_eq!(theme.edge_dim_color, "#5a6070");
assert_eq!(theme.edge_dim_width, 1.3);
assert_eq!(theme.box_select_fill, "#4d90fe");
assert_eq!(theme.box_select_fill_alpha, 0.15);
assert_eq!(theme.box_select_border, "#7fb2ff");
assert_eq!(theme.box_select_border_width, 1.0);
assert_eq!(theme.hover_card.background, FigureTheme::dark().background);
assert_eq!(theme.label_offset_3d_x, 6.0);
assert_eq!(theme.label_offset_3d_y, 4.0);
assert_eq!(theme.cluster_label_extra_offset_3d_x, 10.0);
assert_eq!(theme.grid_tick_font, "10px sans-serif");
assert_eq!(theme.grid_tick_color, "#8a93a6");
}
#[test]
fn default_is_dark() {
assert_eq!(GraphTheme::default().selection_ring_color, GraphTheme::dark().selection_ring_color);
}
#[test]
fn light_fixes_the_invisible_white_on_white_selection_ring_defect() {
let light = GraphTheme::light();
assert_ne!(light.selection_ring_color, "#ffffff", "a white ring must not survive onto the light preset");
assert_ne!(light.label_fill, "#e6e6ea", "the near-white label fill must not survive onto the light preset either");
assert_eq!(light.hover_card.background, FigureTheme::light().background);
}
#[test]
fn high_contrast_uses_pure_black_chrome_ink() {
let hc = GraphTheme::high_contrast();
assert_eq!(hc.label_fill, "#ffffff");
assert_eq!(hc.hover_card.background, FigureTheme::high_contrast().background);
assert_ne!(hc.selection_ring_color, hc.hover_ring_color, "selection and hover rings must stay visually distinct");
}
#[test]
fn every_preset_carries_the_same_default_category_palette() {
let dark = GraphTheme::dark();
let light = GraphTheme::light();
let hc = GraphTheme::high_contrast();
assert_eq!(dark.category_palette.len(), light.category_palette.len());
assert_eq!(dark.category_palette.color_for(0), light.category_palette.color_for(0));
assert_eq!(dark.category_palette.color_for(0), hc.category_palette.color_for(0));
}
#[test]
fn a_caller_can_swap_in_the_okabe_ito_colorblind_safe_palette() {
let theme = GraphTheme { category_palette: CategoricalScale::default_palette(), ..GraphTheme::dark() };
assert_eq!(theme.category_palette.len(), 8);
assert_eq!(theme.category_palette.color_for(0), CategoricalScale::default_palette().color_for(0));
assert_ne!(theme.category_palette.color_for(0), default_category_palette().color_for(0));
}
#[test]
fn every_preset_shares_the_same_3d_overlay_offsets_but_a_distinct_grid_tick_color() {
let dark = GraphTheme::dark();
let light = GraphTheme::light();
let hc = GraphTheme::high_contrast();
for theme in [&dark, &light, &hc] {
assert_eq!(theme.label_offset_3d_x, 6.0);
assert_eq!(theme.label_offset_3d_y, 4.0);
assert_eq!(theme.cluster_label_extra_offset_3d_x, 10.0);
assert_eq!(theme.grid_tick_font, "10px sans-serif");
}
assert_ne!(dark.grid_tick_color, light.grid_tick_color);
assert_ne!(dark.grid_tick_color, hc.grid_tick_color);
}
#[test]
fn default_category_palette_matches_the_named_constant_in_order() {
let palette = default_category_palette();
assert_eq!(palette.len(), DEFAULT_CATEGORY_PALETTE.len());
for (i, &expected) in DEFAULT_CATEGORY_PALETTE.iter().enumerate() {
assert_eq!(palette.color_for(i), expected);
}
}
}