fulgur-chart 0.13.1

Render chart.js-compatible JSON specs to deterministic static SVG/PNG
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! JSON Schema types for the Vega-Lite subset input DSL.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Root type for a Vega-Lite subset spec accepted by fulgur-chart.
/// The `mark` field value selects the per-chart-kind schema variant.
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum VegaLiteSpec {
    Bar(VlBarSpec),
    Line(VlLineSpec),
    Point(VlPointSpec),
    Circle(VlCircleSpec),
    Arc(VlArcSpec),
    Rect(VlRectSpec),
}

// ────────────────────────────────────────────────
// Common helpers
// ────────────────────────────────────────────────

/// Inline data: an array of JSON objects (data.values).
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlData {
    pub values: Vec<serde_json::Value>,
}

/// An encoding channel: a data field reference with an optional type hint.
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlChannel {
    /// Name of the field in each record of data.values.
    pub field: String,
    /// Type hint (e.g. "quantitative", "nominal"). Currently has no effect on rendering.
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub field_type: Option<String>,
}

/// Vega-Lite title: either a plain string or a `{text: ...}` object.
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum VlTitle {
    Text(String),
    Obj { text: String },
}

// ────────────────────────────────────────────────
// Mark constant types
//
// Each mark comes in two forms: a bare string ("bar") and an object with
// a `type` key (`{"type": "bar"}`). The `Mark*Name` enums pin the accepted
// literal, and the `Mark*` untagged wrappers accept either form so the
// generated JSON Schema matches what `parse_mark` in frontend/vegalite.rs
// already accepts.
// ────────────────────────────────────────────────

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MarkBarName {
    Bar,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MarkBarObject {
    #[serde(rename = "type")]
    pub mark_type: MarkBarName,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MarkBar {
    String(MarkBarName),
    Object(MarkBarObject),
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MarkLineName {
    Line,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MarkLineObject {
    #[serde(rename = "type")]
    pub mark_type: MarkLineName,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MarkLine {
    String(MarkLineName),
    Object(MarkLineObject),
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MarkPointName {
    Point,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MarkPointObject {
    #[serde(rename = "type")]
    pub mark_type: MarkPointName,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MarkPoint {
    String(MarkPointName),
    Object(MarkPointObject),
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MarkCircleName {
    Circle,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MarkCircleObject {
    #[serde(rename = "type")]
    pub mark_type: MarkCircleName,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MarkCircle {
    String(MarkCircleName),
    Object(MarkCircleObject),
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MarkArcName {
    Arc,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MarkArcObject {
    #[serde(rename = "type")]
    pub mark_type: MarkArcName,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MarkArc {
    String(MarkArcName),
    Object(MarkArcObject),
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MarkRectName {
    Rect,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MarkRectObject {
    #[serde(rename = "type")]
    pub mark_type: MarkRectName,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MarkRect {
    String(MarkRectName),
    Object(MarkRectObject),
}

// ────────────────────────────────────────────────
// Bar chart (mark: "bar")
// ────────────────────────────────────────────────

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlBarSpec {
    pub mark: MarkBar,
    pub data: VlData,
    pub encoding: VlBarEncoding,
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
    pub schema: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<VlTitle>,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlBarEncoding {
    pub x: VlChannel,
    pub y: VlChannel,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<VlChannel>,
}

// ────────────────────────────────────────────────
// Line chart (mark: "line")
// ────────────────────────────────────────────────

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlLineSpec {
    pub mark: MarkLine,
    pub data: VlData,
    pub encoding: VlBarEncoding,
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
    pub schema: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<VlTitle>,
}

// ────────────────────────────────────────────────
// Scatter plot (mark: "point")
// ────────────────────────────────────────────────

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlPointSpec {
    pub mark: MarkPoint,
    pub data: VlData,
    pub encoding: VlPointEncoding,
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
    pub schema: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<VlTitle>,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlPointEncoding {
    pub x: VlChannel,
    pub y: VlChannel,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<VlChannel>,
}

// ────────────────────────────────────────────────
// Circle plot (mark: "circle")
//
// Always-filled-circle variant of the point mark. `shape` is intentionally
// omitted so that if point ever grows a shape channel, circle stays
// shape-free by structure.
// ────────────────────────────────────────────────

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlCircleSpec {
    pub mark: MarkCircle,
    pub data: VlData,
    pub encoding: VlCircleEncoding,
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
    pub schema: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<VlTitle>,
}

/// Encoding for `mark: "circle"`. No `shape` channel — see section note.
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlCircleEncoding {
    pub x: VlChannel,
    pub y: VlChannel,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<VlChannel>,
}

// ────────────────────────────────────────────────
// Arc / pie chart (mark: "arc")
// ────────────────────────────────────────────────

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlArcSpec {
    pub mark: MarkArc,
    pub data: VlData,
    pub encoding: VlArcEncoding,
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
    pub schema: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<VlTitle>,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlArcEncoding {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub theta: Option<VlChannel>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<VlChannel>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub x: Option<VlChannel>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub y: Option<VlChannel>,
}

// ────────────────────────────────────────────────
// Rect / heatmap chart (mark: "rect")
//
// x/y はカテゴリ、color は quantitative(2色補間)または nominal(パレット割当)。
// encoding.color.aggregate は "mean" / "sum" 列挙で、schema と runtime の受理範囲を揃える。
// ────────────────────────────────────────────────

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlRectSpec {
    pub mark: MarkRect,
    pub data: VlData,
    pub encoding: VlRectEncoding,
    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
    pub schema: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<VlTitle>,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlRectEncoding {
    pub x: VlRectAxisChannel,
    pub y: VlRectAxisChannel,
    pub color: VlRectColorChannel,
}

/// rect の x/y encoding が受理する type。quantitative は binned ヒートマップ
/// 想定で MVP 外のため、schema レベルで nominal / ordinal のみ許可する。
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum VlRectAxisType {
    Nominal,
    Ordinal,
}

/// rect の x/y チャネル。`field` は必須、`type` は nominal/ordinal のみ受理。
/// (`VlChannel` は `type` に任意文字列を許容するが、rect の軸では quantitative
/// は runtime で reject されるため、schema でも同じ範囲に絞っておく。)
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlRectAxisChannel {
    pub field: String,
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub field_type: Option<VlRectAxisType>,
}

/// rect の color.aggregate に許容される集約方式。frontend が受理する "mean"/"sum" と
/// 対応する。runtime は `frontend::vegalite::check_unknown_keys` で同じ値だけを許可し、
/// 他値は strict モードで Err になる。
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum VlRectAggregate {
    Mean,
    Sum,
}

/// rect の color チャネルのうち quantitative variant が受理する type。
/// `Option` と組合わせて `type` 省略も許容する (省略時は infer に委ねる)。
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum VlRectColorQuantitativeType {
    Quantitative,
}

/// rect の color チャネルのうち nominal / ordinal を表現する categorical variant の type。
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum VlRectColorCategoricalType {
    Nominal,
    Ordinal,
}

/// rect の color チャネル (quantitative variant)。
/// `type` は省略可能 (省略時は infer)、`aggregate` は列挙で受理値を絞る。
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlRectColorQuantitativeChannel {
    pub field: String,
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub field_type: Option<VlRectColorQuantitativeType>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aggregate: Option<VlRectAggregate>,
}

/// rect の color チャネル (categorical variant)。
/// `type` は必須 (`nominal` / `ordinal`)、`aggregate` は許容しない
/// (categorical への aggregate は runtime で明示 Err になる)。
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VlRectColorCategoricalChannel {
    pub field: String,
    #[serde(rename = "type")]
    pub field_type: VlRectColorCategoricalType,
}

/// rect の color チャネル。
///
/// - Quantitative: `type` 省略 or `quantitative`、`aggregate` 許容。
/// - Categorical: `type` は `nominal` / `ordinal`、`aggregate` 不可。
///
/// `untagged` で外から見ると同じ JSON 形だが、"quantitative-with-aggregate XOR
/// nominal/ordinal-without-aggregate" の invariant を schema レベルで表現する。
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum VlRectColorChannel {
    Quantitative(VlRectColorQuantitativeChannel),
    Categorical(VlRectColorCategoricalChannel),
}