mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 01:21:59 +00:00
paint.setFill, paint.setStroke, paint.none, Apply Fill to Stroke and Apply Stroke to Fill stored a series' paint on the graph. appearance.setItem, the Eyedropper, appearance.transfer, Clear Appearance, Reduce to Basic Appearance and the add, remove, move and duplicate row commands stored no paint or weight. An Eyedropper pick of a blue fill at 40% onto a red column drew red at 40% after the next redraw. These commands and Show All Hidden Attributes now store the paint and weight of the topmost visible fill and stroke of a path directly in a series group, in the same undo step. A color, weight or opacity that a command writes to that row replaces the stored value even when the mark's own value does not change. A row that has no paint before and after the command does not change the stored paint. These commands store no stroke for a series that draws Filled Lines bands, and a band no longer takes its color from an empty stored stroke. The stored opacity also comes from the topmost visible row: hiding a top row stores the row under it, and an opacity set on a hidden top row is not stored. Follows #5501.
70 lines
3.4 KiB
Rust
70 lines
3.4 KiB
Rust
use super::{GraphSpec, Value};
|
||
|
||
pub(super) const LABEL_SIZE: f64 = 9.0;
|
||
|
||
/// Most tick marks per division (Graph Type › Tick Marks), and most category tick marks a graph draws.
|
||
pub(super) const MAX_TICK_MARKS: usize = 20;
|
||
|
||
pub(super) const MAX_CATEGORY_TICKS: usize = 10_000;
|
||
|
||
/// Longest axis label prefix or suffix (Graph Type › Add Labels).
|
||
const MAX_AFFIX: usize = 64;
|
||
|
||
/// An axis label prefix or suffix: one line (no control characters or line separators), at most [`MAX_AFFIX`] long.
|
||
pub(super) fn one_line(s: &str) -> String {
|
||
s.chars().filter(|c| !c.is_control() && !matches!(c, '\u{2028}' | '\u{2029}')).take(MAX_AFFIX).collect()
|
||
}
|
||
|
||
/// A count parameter: a whole number, also as the float a dialog sends back (3.0), at most `max`.
|
||
pub(super) fn count_param(p: &Value, key: &str, max: u64) -> Option<usize> {
|
||
let v = p.get(key)?;
|
||
let n = v.as_u64().or_else(|| v.as_f64().filter(|x| x.is_finite() && *x >= 0.0).map(|x| x.round().min(max as f64) as u64))?;
|
||
usize::try_from(n.min(max)).ok()
|
||
}
|
||
|
||
/// A "nice" axis: (min, max, step).
|
||
pub(super) fn nice_axis(lo: f64, hi: f64, ticks: usize) -> (f64, f64, f64) {
|
||
let (lo, hi) = if (hi - lo).abs() < 1e-12 { (lo.min(0.0), lo.max(0.0) + 1.0) } else { (lo, hi) };
|
||
let n = if ticks == 0 { 5 } else { ticks.clamp(1, 100) } as f64;
|
||
let raw = (hi - lo) / n;
|
||
let mag = 10f64.powf(raw.log10().floor());
|
||
let step = [1.0, 2.0, 2.5, 5.0, 10.0].iter().map(|m| m * mag).find(|s| *s >= raw - 1e-12).unwrap_or(10.0 * mag);
|
||
((lo / step).floor() * step, (hi / step).ceil() * step, step)
|
||
}
|
||
|
||
/// The value axis of a radar graph: from 0 to the largest value of any series.
|
||
pub(super) fn radar_axis(g: &GraphSpec) -> (f64, f64, f64) {
|
||
value_axis(g.ticks, g.axis_min, g.axis_max, 0.0, g.rows.iter().flatten().copied().fold(0.0, f64::max))
|
||
}
|
||
|
||
/// A value axis: with both `min` and `max` set (Graph Type › Tick Values › Override Calculated Values), exactly that
|
||
/// range split into `ticks` divisions (5 when automatic); otherwise a nice axis around the data, using whichever bound
|
||
/// was given.
|
||
pub(super) fn value_axis(ticks: usize, axis_min: Option<f64>, axis_max: Option<f64>, lo: f64, hi: f64) -> (f64, f64, f64) {
|
||
match (axis_min, axis_max) {
|
||
(Some(min), Some(max)) if max > min && (max - min).is_finite() => {
|
||
let n = if ticks == 0 { 5 } else { ticks.clamp(1, 100) } as f64;
|
||
(min, max, (max - min) / n)
|
||
}
|
||
_ => {
|
||
// An override too wide for f64 (or NaN) gives a non-finite axis, which would reach `clamp` with NaN bounds
|
||
// and panic: fall back to the data, then to 0..1.
|
||
let finite = |(a, b, s): (f64, f64, f64)| a.is_finite() && b.is_finite() && s.is_finite() && b > a && s > 0.0;
|
||
[nice_axis(axis_min.unwrap_or(lo), axis_max.unwrap_or(hi), ticks), nice_axis(lo, hi, ticks)]
|
||
.into_iter()
|
||
.find(|&a| finite(a))
|
||
.unwrap_or((0.0, 1.0, 0.2))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The tick values from `lo` to `hi` every `step`, both ends included: counted, not accumulated, so the last tick
|
||
/// lands on `hi` and a step too small for the range (or not finite) can't loop forever. At most 1000 ticks.
|
||
pub(super) fn tick_values(lo: f64, hi: f64, step: f64) -> Vec<f64> {
|
||
let n = ((hi - lo) / step).round();
|
||
if !(lo.is_finite() && step.is_finite() && step > 0.0 && n.is_finite() && n >= 0.0) {
|
||
return vec![];
|
||
}
|
||
(0..=(n as usize).min(1000)).map(|i| lo + i as f64 * step).collect()
|
||
}
|