Skip to main content

chartml_core/spec/
chart.rs

1use serde::{Deserialize, Serialize};
2
3use super::source::CacheConfig;
4use super::style::{FontsSpec, GridSpec, LegendSpec};
5use super::transform::TransformSpec;
6
7// --- Top-level ChartSpec ---
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
10#[serde(rename_all = "camelCase")]
11pub struct ChartSpec {
12    pub version: u32,
13    pub title: Option<String>,
14    pub data: DataRef,
15    pub transform: Option<TransformSpec>,
16    pub visualize: VisualizeSpec,
17    pub layout: Option<LayoutSpec>,
18    pub style: Option<StyleRefOrInline>,
19    pub params: Option<Vec<super::params::ParamDef>>,
20}
21
22// --- DataRef: either a string reference or inline data ---
23
24#[derive(Debug, Clone, Deserialize, Serialize)]
25#[serde(untagged)]
26pub enum DataRef {
27    Named(String),
28    Inline(InlineData),
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct InlineData {
34    pub provider: String,
35    pub rows: Option<Vec<serde_json::Value>>,
36    pub url: Option<String>,
37    pub endpoint: Option<String>,
38    pub cache: Option<CacheConfig>,
39}
40
41// --- StyleRef for chart-level: string or inline style ---
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44#[serde(untagged)]
45pub enum StyleRefOrInline {
46    Named(String),
47    Inline(Box<ChartStyleSpec>),
48}
49
50// --- VisualizeSpec ---
51
52#[derive(Debug, Clone, Deserialize, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct VisualizeSpec {
55    #[serde(rename = "type")]
56    pub chart_type: String,
57    pub mode: Option<ChartMode>,
58    pub orientation: Option<Orientation>,
59    pub columns: Option<FieldRef>,
60    pub rows: Option<FieldRef>,
61    pub marks: Option<MarksSpec>,
62    pub axes: Option<AxesSpec>,
63    pub annotations: Option<Vec<AnnotationSpec>>,
64    pub style: Option<ChartStyleSpec>,
65    // Metric-specific fields
66    pub value: Option<String>,
67    pub label: Option<String>,
68    pub format: Option<String>,
69    pub compare_with: Option<String>,
70    pub invert_trend: Option<bool>,
71    pub data_labels: Option<DataLabelsSpec>,
72}
73
74// --- ChartMode ---
75
76#[derive(Debug, Clone, Deserialize, Serialize)]
77#[serde(rename_all = "lowercase")]
78pub enum ChartMode {
79    Stacked,
80    Grouped,
81    Normalized,
82}
83
84// --- Orientation ---
85
86#[derive(Debug, Clone, Deserialize, Serialize)]
87#[serde(rename_all = "lowercase")]
88pub enum Orientation {
89    Vertical,
90    Horizontal,
91}
92
93// --- FieldRef ---
94
95#[derive(Debug, Clone, Deserialize, Serialize)]
96#[serde(untagged)]
97pub enum FieldRef {
98    Simple(String),
99    Detailed(Box<FieldSpec>),
100    Multiple(Vec<FieldRefItem>),
101}
102
103/// Items within a FieldRef::Multiple array — each can be a string or a detailed spec.
104#[derive(Debug, Clone, Deserialize, Serialize)]
105#[serde(untagged)]
106pub enum FieldRefItem {
107    Simple(String),
108    Detailed(Box<FieldSpec>),
109}
110
111#[derive(Debug, Clone, Deserialize, Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct FieldSpec {
114    pub field: String,
115    pub mark: Option<String>,
116    pub axis: Option<String>,
117    pub label: Option<String>,
118    pub color: Option<String>,
119    pub format: Option<String>,
120    pub data_labels: Option<DataLabelsSpec>,
121    /// Line style: "solid" (default), "dashed", "dotted"
122    pub line_style: Option<String>,
123    /// For range marks: upper bound field name
124    pub upper: Option<String>,
125    /// For range marks: lower bound field name
126    pub lower: Option<String>,
127    /// Fill opacity for range marks (default 0.15)
128    pub opacity: Option<f64>,
129}
130
131// --- DataLabelsSpec ---
132
133#[derive(Debug, Clone, Deserialize, Serialize)]
134#[serde(rename_all = "camelCase")]
135pub struct DataLabelsSpec {
136    pub show: Option<bool>,
137    pub position: Option<String>,
138    pub format: Option<String>,
139    pub color: Option<String>,
140    pub font_size: Option<f64>,
141}
142
143// --- MarksSpec ---
144
145#[derive(Debug, Clone, Deserialize, Serialize)]
146#[serde(rename_all = "camelCase")]
147pub struct MarksSpec {
148    pub color: Option<MarkEncoding>,
149    pub size: Option<MarkEncoding>,
150    pub shape: Option<MarkEncoding>,
151    pub text: Option<MarkEncoding>,
152}
153
154#[derive(Debug, Clone, Deserialize, Serialize)]
155#[serde(untagged)]
156pub enum MarkEncoding {
157    Simple(String),
158    Detailed(MarkEncodingSpec),
159}
160
161#[derive(Debug, Clone, Deserialize, Serialize)]
162#[serde(rename_all = "camelCase")]
163pub struct MarkEncodingSpec {
164    pub field: String,
165    pub label: Option<String>,
166    pub format: Option<String>,
167}
168
169// --- AxesSpec ---
170
171#[derive(Debug, Clone, Deserialize, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct AxesSpec {
174    #[serde(alias = "columns", alias = "bottom")]
175    pub x: Option<AxisSpec>,
176    #[serde(alias = "rows")]
177    pub left: Option<AxisSpec>,
178    pub right: Option<AxisSpec>,
179}
180
181#[derive(Debug, Clone, Deserialize, Serialize)]
182#[serde(rename_all = "camelCase")]
183pub struct AxisSpec {
184    pub label: Option<String>,
185    pub format: Option<String>,
186    pub min: Option<f64>,
187    pub max: Option<f64>,
188    pub nice: Option<bool>,
189}
190
191// --- AnnotationSpec ---
192
193#[derive(Debug, Clone, Deserialize, Serialize)]
194#[serde(rename_all = "camelCase")]
195pub struct AnnotationSpec {
196    #[serde(rename = "type")]
197    pub annotation_type: String,
198    pub axis: Option<String>,
199    pub value: Option<serde_json::Value>,
200    pub from: Option<serde_json::Value>,
201    pub to: Option<serde_json::Value>,
202    pub orientation: Option<String>,
203    pub label: Option<String>,
204    pub label_position: Option<String>,
205    pub color: Option<String>,
206    pub stroke_width: Option<f64>,
207    pub dash_array: Option<String>,
208    pub opacity: Option<f64>,
209    pub stroke_color: Option<String>,
210    /// Line style shorthand: "solid" (default), "dashed", "dotted"
211    pub style: Option<String>,
212}
213
214// --- ChartStyleSpec ---
215
216#[derive(Debug, Clone, Deserialize, Serialize)]
217#[serde(rename_all = "camelCase")]
218pub struct ChartStyleSpec {
219    pub height: Option<f64>,
220    pub width: Option<f64>,
221    /// Color palette for chart series.
222    ///
223    /// Colors are assigned **per series**, not per category/bar:
224    /// - Single-series chart (no `marks.color`): ALL bars/points use `colors[0]`
225    /// - Multi-series chart (`marks.color` or multiple `rows`): each series gets
226    ///   `colors[series_index]`, cycling if more series than colors
227    ///
228    /// This matches the JS chartml behavior (d3ChartMapper.js lines 139-146).
229    pub colors: Option<Vec<String>>,
230    pub grid: Option<GridSpec>,
231    pub show_dots: Option<bool>,
232    pub stroke_width: Option<f64>,
233    pub curve_type: Option<String>,
234    pub fill_opacity: Option<f64>,
235    pub fonts: Option<FontsSpec>,
236    pub legend: Option<LegendSpec>,
237}
238
239// --- LayoutSpec ---
240
241#[derive(Debug, Clone, Deserialize, Serialize)]
242#[serde(rename_all = "camelCase")]
243pub struct LayoutSpec {
244    pub col_span: Option<u32>,
245}