1use super::layout::{Layout, Map};
4use crate::scale::{Scale, Unit};
5
6#[derive(Debug, Clone)]
25pub struct Mapping {
26 left: usize,
28 top: usize,
29 columns: usize,
30 rows: usize,
31 px: usize,
33 py: usize,
34 x: Map,
35 y: Map,
36 x_domain: (f64, f64),
37 y_domain: (f64, f64),
38 x_kind: AxisKind,
39 y_kind: AxisKind,
40 x_categories: Option<Vec<String>>,
41 y_categories: Option<Vec<String>>,
42 x_unit: Unit,
43 y_unit: Unit,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct Panel {
52 pub column: usize,
54 pub row: usize,
56 pub width: usize,
58 pub height: usize,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub(crate) enum AxisKind {
64 Linear,
65 Log,
66 Time,
67 Bands,
68}
69
70impl Mapping {
71 pub(crate) fn new(layout: &Layout<'_>, x_spec: &Scale, y_spec: &Scale) -> Mapping {
72 let x_kind = if layout.band.is_some() {
73 AxisKind::Bands
74 } else {
75 AxisKind::of(x_spec)
76 };
77 let y_kind = if layout.y_band.is_some() {
78 AxisKind::Bands
79 } else {
80 AxisKind::of(y_spec)
81 };
82 let y_categories = layout.y_categories.map(<[String]>::to_vec);
83 Mapping {
84 left: layout.gutter,
85 top: layout.plot_top,
86 columns: layout.plot_cols,
87 rows: layout.plot_rows,
88 px: layout.px.max(1),
89 py: layout.py.max(1),
90 x: layout.x_scale,
91 y: layout.y_scale,
92 x_domain: layout.x_domain,
93 y_domain: layout.y_domain,
94 x_kind,
95 y_kind,
96 x_categories: layout.categories.map(<[String]>::to_vec),
97 y_categories,
98 x_unit: layout.x_unit.clone(),
99 y_unit: layout.y_unit.clone(),
100 }
101 }
102
103 pub(crate) fn empty() -> Mapping {
105 Mapping {
106 left: 0,
107 top: 0,
108 columns: 0,
109 rows: 0,
110 px: 1,
111 py: 1,
112 x: Map::build((0.0, 1.0), (0.0, 1.0), false),
113 y: Map::build((0.0, 1.0), (1.0, 0.0), false),
114 x_domain: (0.0, 1.0),
115 y_domain: (0.0, 1.0),
116 x_kind: AxisKind::Linear,
117 y_kind: AxisKind::Linear,
118 x_categories: None,
119 y_categories: None,
120 x_unit: Unit::Plain,
121 y_unit: Unit::Plain,
122 }
123 }
124
125 pub fn plot_area(&self) -> Option<Panel> {
128 (self.columns > 0 && self.rows > 0).then_some(Panel {
129 column: self.left,
130 row: self.top,
131 width: self.columns,
132 height: self.rows,
133 })
134 }
135
136 pub fn data_at(&self, column: usize, row: usize) -> Option<(f64, f64)> {
143 if self.columns == 0 || self.rows == 0 {
144 return None;
145 }
146 let inside = (self.left..self.left + self.columns).contains(&column)
147 && (self.top..self.top + self.rows).contains(&row);
148 if !inside {
149 return None;
150 }
151 let sub_x = ((column - self.left) * self.px) as f64 + (self.px as f64 - 1.0) / 2.0;
152 let sub_y = ((row - self.top) * self.py) as f64 + (self.py as f64 - 1.0) / 2.0;
153 Some((self.x.unmap(sub_x), self.y.unmap(sub_y)))
154 }
155
156 pub fn column_at(&self, x: f64) -> Option<usize> {
162 if self.columns == 0 || self.rows == 0 {
163 return None;
164 }
165 let sub_x = self.x.map(x).round();
166 if !sub_x.is_finite() || sub_x < 0.0 {
167 return None;
168 }
169 let column = self.left + sub_x as usize / self.px;
170 (column < self.left + self.columns).then_some(column)
171 }
172
173 pub fn cell_at(&self, x: f64, y: f64) -> Option<(usize, usize)> {
176 if self.columns == 0 || self.rows == 0 {
177 return None;
178 }
179 let sub_x = self.x.map(x).round();
180 let sub_y = self.y.map(y).round();
181 if !(sub_x.is_finite() && sub_y.is_finite()) || sub_x < 0.0 || sub_y < 0.0 {
182 return None;
183 }
184 let column = self.left + sub_x as usize / self.px;
185 let row = self.top + sub_y as usize / self.py;
186 (column < self.left + self.columns && row < self.top + self.rows).then_some((column, row))
187 }
188
189 pub fn x_span_at(&self, column: usize) -> Option<(f64, f64)> {
192 if self.columns == 0 || !(self.left..self.left + self.columns).contains(&column) {
193 return None;
194 }
195 let start = ((column - self.left) * self.px) as f64 - 0.5;
196 let (a, b) = (self.x.unmap(start), self.x.unmap(start + self.px as f64));
197 Some((a.min(b), a.max(b)))
198 }
199
200 pub fn y_span_at(&self, row: usize) -> Option<(f64, f64)> {
203 if self.rows == 0 || !(self.top..self.top + self.rows).contains(&row) {
204 return None;
205 }
206 let start = ((row - self.top) * self.py) as f64 - 0.5;
207 let (a, b) = (self.y.unmap(start), self.y.unmap(start + self.py as f64));
208 Some((a.min(b), a.max(b)))
209 }
210
211 pub fn x_domain(&self) -> (f64, f64) {
215 self.x_domain
216 }
217
218 pub fn y_domain(&self) -> (f64, f64) {
220 self.y_domain
221 }
222
223 pub fn x_categories(&self) -> Option<&[String]> {
227 if matches!(self.x_kind, AxisKind::Bands) {
228 self.x_categories.as_deref()
229 } else {
230 None
231 }
232 }
233
234 pub fn y_categories(&self) -> Option<&[String]> {
237 if matches!(self.y_kind, AxisKind::Bands) {
238 self.y_categories.as_deref()
239 } else {
240 None
241 }
242 }
243
244 pub fn format_x(&self, value: f64) -> String {
249 format_value(
250 value,
251 self.x_kind,
252 self.x_domain,
253 self.columns,
254 self.x_categories.as_deref(),
255 &self.x_unit,
256 )
257 }
258
259 pub fn format_y(&self, value: f64) -> String {
261 format_value(
262 value,
263 self.y_kind,
264 self.y_domain,
265 self.rows,
266 self.y_categories.as_deref(),
267 &self.y_unit,
268 )
269 }
270
271 pub fn viewport(&self) -> crate::plot::Viewport {
275 crate::plot::Viewport::seeded(
276 (self.x_kind != AxisKind::Bands).then_some(self.x_domain),
277 (self.y_kind != AxisKind::Bands).then_some(self.y_domain),
278 self.x_kind == AxisKind::Log,
279 self.y_kind == AxisKind::Log,
280 )
281 }
282}
283
284impl AxisKind {
285 fn of(spec: &Scale) -> AxisKind {
286 match spec {
287 Scale::Log => AxisKind::Log,
288 Scale::Time => AxisKind::Time,
289 _ => AxisKind::Linear,
290 }
291 }
292}
293
294fn format_value(
297 value: f64,
298 kind: AxisKind,
299 domain: (f64, f64),
300 cells: usize,
301 categories: Option<&[String]>,
302 unit: &Unit,
303) -> String {
304 if !value.is_finite() {
305 return value.to_string();
306 }
307 match kind {
308 AxisKind::Bands => {
309 let index = value.round();
310 match categories {
311 Some(categories) if !categories.is_empty() => {
312 let index = (index.max(0.0) as usize).min(categories.len() - 1);
313 categories[index].clone()
314 }
315 _ => index.to_string(),
316 }
317 }
318 AxisKind::Time => {
319 crate::scale::time::readout(value, (domain.1 - domain.0) / cells.max(1) as f64)
320 }
321 AxisKind::Linear => {
322 let step = (domain.1 - domain.0) / cells.max(1) as f64;
323 match unit {
324 Unit::Plain => decimal_at(value, step),
325 Unit::Suffix(suffix) => format!("{}{suffix}", decimal_at(value, step)),
326 Unit::Si(name) => {
327 let magnitude = if value == 0.0 {
330 0
331 } else {
332 value.abs().log10().floor() as i32
333 };
334 match crate::scale::format::si_prefix(magnitude) {
335 Some((shift, prefix)) => {
336 let factor = 10f64.powi(shift);
337 format!(
338 "{} {prefix}{name}",
339 decimal_at(value / factor, step / factor)
340 )
341 }
342 None => format!("{} {name}", decimal_at(value, step)),
343 }
344 }
345 Unit::Bytes => {
346 let (power, factor) = crate::scale::unit::binary_prefix(value.abs());
347 format!(
348 "{} {}",
349 decimal_at(value / factor, step / factor),
350 crate::scale::unit::BINARY_UNITS[power]
351 )
352 }
353 }
354 }
355 AxisKind::Log => {
356 if value <= 0.0 || domain.0 <= 0.0 || domain.1 <= 0.0 {
357 return value.to_string();
358 }
359 let per_cell = (domain.1.log10() - domain.0.log10()) / cells.max(1) as f64;
360 decimal_at(value, value * std::f64::consts::LN_10 * per_cell)
361 }
362 }
363}
364
365fn decimal_at(value: f64, step: f64) -> String {
368 let decimals = if step.is_finite() && step > 0.0 {
369 (-step.log10()).ceil().clamp(0.0, 12.0) as i32
370 } else {
371 3
372 };
373 let scaled = value * 10f64.powi(decimals);
374 if scaled.abs() >= 1e15 {
375 return crate::scale::NumberFormat::for_values(&[value]).format(value);
378 }
379 crate::scale::format::decimal(scaled.round() as i128, -decimals)
380}
381
382#[cfg(test)]
383#[path = "tests/mapping_tests.rs"]
384mod tests;