Files
gridcraft/crates/chart/src/lib.rs
T

159 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! GridCraft charts (layer L3, toolkit-free).
//!
//! - [`resolve`] pulls series names, categories and values out of a workbook,
//! - [`render`] lays a chart out into a list of drawing primitives ([`Prim`]) that any UI
//! toolkit (or the built-in [`rasterize`]) can paint,
//! - [`render_sparkline`] draws in-cell sparklines,
//! - [`rasterize`] turns primitives into RGBA pixels for PNG export and tests.
//!
//! The look (palette, greys, spacing) is GridCraft's own design.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
mod axis;
mod raster;
mod render;
mod resolve;
mod sparkline;
#[cfg(test)]
mod tests;
pub use gridcraft_model::{Chart, ChartKind, LegendPos, SparklineKind};
use serde::{Deserialize, Serialize};
pub use axis::{Scale, format_number, nice_scale};
pub use raster::rasterize;
pub use render::{Swatch, legend_entries, render};
pub use resolve::{default_palette, resolve, series_color};
pub use sparkline::render_sparkline;
/// Straight (non-premultiplied) RGBA colour.
pub type Rgba = [u8; 4];
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum HAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum VAlign {
#[default]
Top,
Middle,
Bottom,
}
/// A drawing primitive in chart coordinates (origin top-left, y down, units = points/pixels).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Prim {
Rect {
x: f32,
y: f32,
w: f32,
h: f32,
fill: Option<Rgba>,
stroke: Option<(Rgba, f32)>,
radius: f32,
},
Line {
pts: Vec<[f32; 2]>,
color: Rgba,
width: f32,
dash: bool,
},
Polygon {
pts: Vec<[f32; 2]>,
fill: Rgba,
stroke: Option<(Rgba, f32)>,
},
/// Pie/doughnut sector: centre, radii, angles in radians (0 = 12 o'clock, clockwise).
Wedge {
cx: f32,
cy: f32,
r_outer: f32,
r_inner: f32,
a0: f32,
a1: f32,
fill: Rgba,
stroke: Option<(Rgba, f32)>,
},
Circle {
cx: f32,
cy: f32,
r: f32,
fill: Option<Rgba>,
stroke: Option<(Rgba, f32)>,
},
/// Text anchored at `(x, y)` per `align`/`valign`, rotated by `rotation` radians about the anchor.
Text {
x: f32,
y: f32,
text: String,
size: f32,
color: Rgba,
bold: bool,
align: HAlign,
valign: VAlign,
rotation: f32,
},
}
/// Measures text width (the UI passes real font metrics; tests use a fixed-width approximation).
pub trait Measure {
fn text_width(&self, text: &str, size: f32, bold: bool) -> f32;
}
/// Fixed-width approximation: `0.55 × size` per character (bold 10% wider).
#[derive(Clone, Copy, Debug, Default)]
pub struct ApproxMeasure;
impl Measure for ApproxMeasure {
fn text_width(&self, text: &str, size: f32, bold: bool) -> f32 {
let n = text.chars().count() as f32;
n * 0.55 * size * if bold { 1.1 } else { 1.0 }
}
}
/// Resolved data: series names, categories and values pulled from the workbook.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ChartData {
pub categories: Vec<String>,
pub series: Vec<SeriesData>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SeriesData {
pub name: String,
pub values: Vec<Option<f64>>,
/// Numeric X values (scatter, bubble).
pub x: Option<Vec<Option<f64>>>,
/// Bubble sizes.
pub sizes: Option<Vec<Option<f64>>>,
pub color: Rgba,
/// Per-series kind (combo charts).
pub kind: ChartKind,
/// Plotted against the secondary (right) value axis.
pub secondary: bool,
/// Number format code of the source values ("General" by default).
pub number_format: String,
}
impl Default for SeriesData {
fn default() -> Self {
SeriesData {
name: String::new(),
values: vec![],
x: None,
sizes: None,
color: [0x15, 0x60, 0x82, 0xFF],
kind: ChartKind::ColumnClustered,
secondary: false,
number_format: "General".into(),
}
}
}