1use crate::base::{CellRef, RangeRef, Value, category_levels, number_to_text};
4use crate::calc::evaluate;
5use crate::model::{Chart, ChartKind, Theme, Workbook, style::apply_tint};
6use crate::numfmt::{NumberFormat, format_value};
7
8use crate::chart::{ChartData, Rgba, SeriesData, TrendlineData};
9
10pub(crate) const MAX_POINTS: usize = 100_000;
12pub(crate) const MAX_SERIES: usize = 255;
14pub(crate) const MAX_TRENDLINES: usize = 16;
16
17const CYCLE_TINTS: [f64; 6] = [0.0, 0.4, -0.25, 0.6, -0.5, 0.2];
19pub(crate) const PALETTE_CYCLE: usize = 6 * CYCLE_TINTS.len();
21
22pub fn series_color(theme: &Theme, i: usize) -> Rgba {
24 let accent = i % 6;
25 let cycle = (i / 6) % CYCLE_TINTS.len();
26 let base = theme.colors.get(4 + accent).copied().unwrap_or(0x156082);
27 let rgb = [(base >> 16) as u8, (base >> 8) as u8, base as u8];
28 let tint = CYCLE_TINTS.get(cycle).copied().unwrap_or(0.0);
29 let [r, g, b] = apply_tint(rgb, tint);
30 [r, g, b, 0xFF]
31}
32
33pub fn default_palette(theme: &Theme, n: usize) -> Vec<Rgba> {
35 (0..n.min(4096)).map(|i| series_color(theme, i)).collect()
36}
37
38fn parse_ref(wb: &Workbook, sheet: usize, formula: &str) -> Option<(usize, RangeRef)> {
40 let f = formula.trim().trim_start_matches('=').trim();
41 let (sh, addr) = match f.rfind('!') {
42 Some(i) => {
43 let name = f.get(..i)?.trim();
44 let name = crate::base::unquote_sheet(name);
45 (wb.sheet_index(&name)?, f.get(i + 1..)?)
46 }
47 None => (sheet, f),
48 };
49 let r = RangeRef::parse(addr)?;
50 Some((sh, r))
51}
52
53fn union_parts(f: &str) -> Option<Vec<&str>> {
55 let inner = f.strip_prefix('(')?.strip_suffix(')')?;
56 let mut parts = Vec::new();
57 let (mut depth, mut quoted, mut start) = (0i32, false, 0usize);
58 for (i, c) in inner.char_indices() {
59 match c {
60 '\'' => quoted = !quoted,
61 '(' if !quoted => depth += 1,
62 ')' if !quoted => depth -= 1,
63 ',' if !quoted && depth == 0 => {
64 parts.push(inner.get(start..i)?);
65 start = i + 1;
66 }
67 _ => {}
68 }
69 if depth < 0 {
70 return None;
71 }
72 }
73 parts.push(inner.get(start..)?);
74 (parts.len() > 1).then_some(parts)
75}
76
77fn eval_list(wb: &Workbook, sheet: usize, formula: &str) -> Vec<Value> {
80 eval_list_in(wb, sheet, formula, 0)
81}
82
83fn eval_list_in(wb: &Workbook, sheet: usize, formula: &str, depth: u32) -> Vec<Value> {
84 let f = formula.trim();
85 if f.is_empty() {
86 return vec![];
87 }
88 if depth < 4
89 && let Some(parts) = union_parts(f)
90 {
91 let mut out = Vec::new();
92 for p in parts {
93 if out.len() >= MAX_POINTS {
94 break;
95 }
96 let rest = MAX_POINTS - out.len();
97 out.extend(eval_list_in(wb, sheet, p, depth + 1).into_iter().take(rest));
98 }
99 return out;
100 }
101 match evaluate(wb, sheet, CellRef::default(), f) {
102 Value::Array(a) => a.iter().take(MAX_POINTS).cloned().collect(),
103 v => vec![v],
104 }
105}
106
107fn to_num(v: &Value) -> Option<f64> {
108 match v {
109 Value::Number(n) if n.is_finite() => Some(*n),
110 _ => None,
111 }
112}
113
114fn nums(wb: &Workbook, sheet: usize, formula: &str) -> Vec<Option<f64>> {
115 eval_list(wb, sheet, formula).iter().map(to_num).collect()
116}
117
118fn ref_format(wb: &Workbook, sheet: usize, formula: &str) -> String {
120 parse_ref(wb, sheet, formula)
121 .and_then(|(sh, r)| wb.sheet(sh).map(|s| wb.styles.get(s.style_id(r.start)).num_fmt.0.to_string()))
122 .unwrap_or_else(|| "General".into())
123}
124
125fn value_text(v: &Value) -> String {
126 match v {
127 Value::Empty => String::new(),
128 Value::Number(n) => number_to_text(*n),
129 Value::Text(t) => t.to_string(),
130 Value::Bool(b) => if *b { "TRUE" } else { "FALSE" }.into(),
131 Value::Error(e) => e.as_str().into(),
132 Value::Array(a) => a.get(0, 0).map(value_text).unwrap_or_default(),
133 }
134}
135
136fn category_table(wb: &Workbook, sheet: usize, formula: &str, values: &str) -> (Vec<String>, Vec<Vec<String>>) {
141 let vals = eval_list(wb, sheet, formula);
142 let cells: Option<(usize, RangeRef)> = parse_ref(wb, sheet, formula);
143 let src = cells.and_then(|(sh, r)| wb.sheet(sh).map(|s| (s, r)));
144 let texts: Vec<String> = vals
145 .iter()
146 .enumerate()
147 .map(|(i, v)| {
148 if let (Some((s, r)), Value::Number(_)) = (src, v) {
149 let w = r.width().max(1) as usize;
150 let at = r.start.offset((i / w) as i64, (i % w) as i64);
151 if let Some(c) = at {
152 let code = &wb.styles.get(s.style_id(c)).num_fmt.0;
153 if !code.eq_ignore_ascii_case("general") {
154 return format_value(v, &NumberFormat::parse_cached(code), wb.date_system).text;
155 }
156 }
157 }
158 value_text(v)
159 })
160 .collect();
161 let Some((_, r)) = src else { return (texts, vec![]) };
162 let value_range = parse_ref(wb, sheet, values).map(|(_, r)| r);
163 let Some(levels) = category_levels(&r, value_range.as_ref()) else { return (texts, vec![]) };
164 let (w, h) = (r.width().max(1) as usize, r.height().max(1) as usize);
165 if texts.len() != w.saturating_mul(h) {
166 return (texts, vec![]); }
168 let at = |row: usize, col: usize| texts.get(row * w + col).cloned().unwrap_or_default();
169 let column = |c: usize| (0..h).map(|row| at(row, c)).collect::<Vec<String>>();
170 let row = |rr: usize| (0..w).map(|c| at(rr, c)).collect::<Vec<String>>();
171 let outer = levels.outer as usize;
172 if levels.by_columns {
173 (column(w - 1), (1..=outer).filter_map(|k| (w - 1).checked_sub(k)).map(column).collect())
174 } else {
175 (row(h - 1), (1..=outer).filter_map(|k| (h - 1).checked_sub(k)).map(row).collect())
176 }
177}
178
179fn is_defined_name(wb: &Workbook, sheet: usize, t: &str) -> bool {
182 match crate::base::split_sheet(t) {
184 (Some(s), bare) => wb.sheet_index(&s).is_some_and(|i| wb.name(bare, i).is_some()),
185 (None, _) => wb.name(t, sheet).is_some(),
186 }
187}
188
189fn series_name(wb: &Workbook, sheet: usize, name: &str) -> String {
192 let t = name.trim();
193 if let Some(q) = t.strip_prefix('"').and_then(|q| q.strip_suffix('"')) {
194 return q.replace("\"\"", "\"");
195 }
196 if t.starts_with('=') || parse_ref(wb, sheet, t).is_some() && t.contains('!') || is_defined_name(wb, sheet, t) {
197 let vals = eval_list(wb, sheet, t);
198 let parts: Vec<String> = vals.iter().map(value_text).filter(|s| !s.is_empty()).take(8).collect();
199 return parts.join(" ");
200 }
201 t.to_string()
202}
203
204pub(crate) fn effective_kind(chart_kind: ChartKind, series_kind: Option<ChartKind>, i: usize) -> ChartKind {
205 match (chart_kind, series_kind) {
206 (ChartKind::Combo, Some(k)) if k != ChartKind::Combo => k,
207 (ChartKind::Combo, _) => {
208 if i == 0 {
209 ChartKind::ColumnClustered
210 } else {
211 ChartKind::Line
212 }
213 }
214 (_, Some(k)) if k != ChartKind::Combo => k,
215 (k, _) => k,
216 }
217}
218
219pub(crate) fn plot_kind(chart_kind: ChartKind, series_kind: Option<ChartKind>, i: usize) -> ChartKind {
222 if chart_kind == ChartKind::Combo { effective_kind(chart_kind, series_kind, i) } else { chart_kind }
223}
224
225pub fn series_plot_kind(chart: &Chart, i: usize) -> ChartKind {
227 plot_kind(chart.kind, chart.series.get(i).and_then(|s| s.kind), i)
228}
229
230pub fn resolve(wb: &Workbook, sheet: usize, chart: &Chart) -> ChartData {
234 let numeric_x = matches!(chart.kind, ChartKind::Scatter | ChartKind::ScatterLines | ChartKind::Bubble);
235 let mut series = Vec::new();
236 let mut categories: Option<Vec<String>> = None;
237 let mut category_levels = Vec::new();
238 for (i, s) in chart.series.iter().take(MAX_SERIES).enumerate() {
239 let values = nums(wb, sheet, &s.values);
240 let name = s.name.as_deref().map(|n| series_name(wb, sheet, n)).filter(|n| !n.is_empty()).unwrap_or_else(|| format!("Series{}", i + 1));
241 let color =
242 s.color.as_ref().and_then(|c| c.resolve(&wb.theme)).map(|[r, g, b]| [r, g, b, 0xFF]).unwrap_or_else(|| series_color(&wb.theme, i));
243 let kind = effective_kind(chart.kind, s.kind, i);
244 let x = match (&s.categories, numeric_x || matches!(kind, ChartKind::Scatter | ChartKind::ScatterLines | ChartKind::Bubble)) {
245 (Some(f), true) => Some(eval_list(wb, sheet, f)).filter(|xs| !xs.iter().any(Value::is_text)).map(|xs| xs.iter().map(to_num).collect()),
247 _ => None,
248 };
249 if categories.is_none()
250 && let Some(f) = &s.categories
251 {
252 let (c, outer) = category_table(wb, sheet, f, &s.values);
253 if !c.is_empty() {
254 categories = Some(c);
255 category_levels = outer;
256 }
257 }
258 let sizes = s.bubble_sizes.as_deref().map(|f| nums(wb, sheet, f));
259 let trendlines = s
260 .trendlines
261 .iter()
262 .take(MAX_TRENDLINES)
263 .map(|t| TrendlineData {
264 spec: t.clone(),
265 color: t.color.as_ref().and_then(|c| c.resolve(&wb.theme)).map(|[r, g, b]| [r, g, b, 0xFF]).unwrap_or(color),
266 })
267 .collect();
268 series.push(SeriesData {
269 name,
270 values,
271 x,
272 sizes,
273 color,
274 kind,
275 secondary: s.secondary,
276 smooth: s.smooth,
277 trendlines,
278 number_format: ref_format(wb, sheet, &s.values),
279 });
280 }
281 let n = series.iter().map(|s| s.values.len()).max().unwrap_or(0);
282 let categories = categories.unwrap_or_else(|| (1..=n).map(|i| i.to_string()).collect());
283 let palette = default_palette(&wb.theme, n.min(PALETTE_CYCLE));
284 ChartData { categories, category_levels, series, palette }
285}