1pub mod area;
8pub mod bar;
9pub mod dot;
10pub mod line;
11pub mod pie;
12
13use crate::font::metrics::StandardFontMetrics;
14use crate::font::StandardFont;
15use crate::style::Color;
16
17#[derive(Debug, Clone)]
19pub enum ChartPrimitive {
20 Rect {
22 x: f64,
23 y: f64,
24 w: f64,
25 h: f64,
26 fill: Color,
27 },
28 Line {
30 x1: f64,
31 y1: f64,
32 x2: f64,
33 y2: f64,
34 stroke: Color,
35 width: f64,
36 },
37 Polyline {
39 points: Vec<(f64, f64)>,
40 stroke: Color,
41 width: f64,
42 },
43 FilledPath {
45 points: Vec<(f64, f64)>,
46 fill: Color,
47 opacity: f64,
48 },
49 Circle {
51 cx: f64,
52 cy: f64,
53 r: f64,
54 fill: Color,
55 },
56 ArcSector {
58 cx: f64,
59 cy: f64,
60 r: f64,
61 start_angle: f64,
62 end_angle: f64,
63 fill: Color,
64 },
65 Label {
67 text: String,
68 x: f64,
69 y: f64,
70 font_size: f64,
71 color: Color,
72 anchor: TextAnchor,
73 },
74}
75
76#[derive(Debug, Clone, Copy)]
78pub enum TextAnchor {
79 Left,
80 Center,
81 Right,
82}
83
84pub const DEFAULT_COLORS: &[&str] = &[
88 "#1a365d", "#2b6cb0", "#3182ce", "#4299e1", "#63b3ed", "#90cdf4", "#e53e3e", "#dd6b20",
89 "#38a169", "#805ad5",
90];
91
92pub const Y_AXIS_WIDTH: f64 = 28.0;
93pub const X_AXIS_HEIGHT: f64 = 20.0;
94pub const AXIS_LABEL_FONT: f64 = 8.0;
95pub const LABEL_MARGIN: f64 = 4.0;
96pub const TITLE_FONT: f64 = 11.0;
97pub const TITLE_HEIGHT: f64 = 20.0;
98pub const GRID_COLOR: Color = Color {
99 r: 0.88,
100 g: 0.88,
101 b: 0.88,
102 a: 1.0,
103};
104pub const AXIS_COLOR: Color = Color {
105 r: 0.4,
106 g: 0.4,
107 b: 0.4,
108 a: 1.0,
109};
110pub const LABEL_COLOR: Color = Color {
111 r: 0.3,
112 g: 0.3,
113 b: 0.3,
114 a: 1.0,
115};
116
117fn helvetica_metrics() -> StandardFontMetrics {
121 StandardFont::Helvetica.metrics()
122}
123
124pub fn measure_label(text: &str, font_size: f64) -> f64 {
126 helvetica_metrics().measure_string(text, font_size, 0.0)
127}
128
129pub fn nice_number(value: f64) -> f64 {
131 if value <= 0.0 {
132 return 1.0;
133 }
134 let exp = value.log10().floor();
135 let frac = value / 10.0_f64.powf(exp);
136 let nice = if frac <= 1.0 {
137 1.0
138 } else if frac <= 2.0 {
139 2.0
140 } else if frac <= 5.0 {
141 5.0
142 } else {
143 10.0
144 };
145 nice * 10.0_f64.powf(exp)
146}
147
148pub fn format_number(value: f64) -> String {
150 if value.abs() >= 1_000_000.0 {
151 format!("{:.1}M", value / 1_000_000.0)
152 } else if value.abs() >= 1_000.0 {
153 format!("{:.1}K", value / 1_000.0)
154 } else if value == value.floor() {
155 format!("{}", value as i64)
156 } else {
157 format!("{:.1}", value)
158 }
159}
160
161pub fn lighten_color(color: &Color, factor: f64) -> Color {
163 Color {
164 r: color.r + (1.0 - color.r) * factor,
165 g: color.g + (1.0 - color.g) * factor,
166 b: color.b + (1.0 - color.b) * factor,
167 a: color.a,
168 }
169}
170
171pub fn parse_hex_color(hex: &str) -> Color {
173 let hex = hex.trim_start_matches('#');
174 match hex.len() {
175 3 => {
176 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).unwrap_or(0);
177 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).unwrap_or(0);
178 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).unwrap_or(0);
179 Color::rgb(r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0)
180 }
181 6 => {
182 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
183 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
184 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
185 Color::rgb(r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0)
186 }
187 _ => Color::BLACK,
188 }
189}
190
191pub fn resolve_color(custom: Option<&str>, index: usize) -> Color {
193 match custom {
194 Some(c) => parse_hex_color(c),
195 None => parse_hex_color(DEFAULT_COLORS[index % DEFAULT_COLORS.len()]),
196 }
197}