tikra 1.2.0

TikZ figures to SVG and PNG with no TeX install; math labels by mathra.
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
//! pgfplots on the engine, as `pgfplots.code.tex` works: `\begin{axis}` opens a survey phase in
//! which `\addplot` computes and stores its coordinates and TikZ statements are put aside; at
//! `\end{axis}` the limits, the scaling and the ticks are computed, then the axis, the plots (in
//! the order they came, with the TikZ statements among them), the descriptions and the legend
//! are drawn ([`draw`]).

pub mod attime;
pub mod boxplot;
pub mod colorbar;
pub mod colormap;
pub mod contour;
pub mod cube;
pub mod dateplot;
pub mod draw;
pub mod fillbetween;
pub mod fpu;
pub mod group;
pub mod keys;
pub mod legend;
pub mod marks;
pub mod mesh;
pub mod number;
pub mod patch;
pub mod plot;
pub mod polar;
pub mod scaling;
pub mod smith;
pub mod symbolic;
pub mod table;
pub mod ternary;
pub mod ticks;
pub mod visual;

use crate::tex::{self, Cat, Meaning, Token};
use crate::Error;

pub use draw::Frame;
pub use plot::{Coord, Plot};

use super::{tikra_tokens, Engine};

/// What the survey phase of an axis collected.
#[derive(Debug, Clone, Default)]
pub struct Axis {
    /// `axis`, `semilogxaxis`, `semilogyaxis`, `loglogaxis`.
    pub kind: String,
    /// Plots and TikZ statements, in order.
    pub entries: Vec<Entry>,
    /// `\addlegendentry` texts with their options.
    pub legend: Vec<LegendEntry>,
    /// The legend images: one per plot that is not forgotten, and `\addlegendimage`s, in order.
    pub images: Vec<Vec<Token>>,
    /// Plots that count (not `forget plot`): the index into the cycle list.
    pub numplots: usize,
    /// Whether a picture was opened for this axis (an `axis` outside `tikzpicture`).
    pub opened_picture: bool,
    /// The axis' options, as given.
    pub options: Vec<Token>,
    /// The environment's group depth, to close it at `\end{axis}`.
    pub depth: usize,
}

#[derive(Debug, Clone)]
pub enum Entry {
    Plot(Box<Plot>),
    /// A TikZ statement (`\draw …;`, `\node …;`), run once the axis is set up.
    Tikz(Vec<Token>),
}

#[derive(Debug, Clone)]
pub struct LegendEntry {
    pub options: Vec<Token>,
    pub text: Vec<Token>,
}

/// Ids of pgfplots' commands (`Meaning::Native`), between the basic layer's and the keys.
pub const BASE: u16 = 800;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum C {
    Addplot,
    Addlegendentry,
    Addlegendentryexpanded,
    Legend,
    Addlegendimage,
    Pgfplotsextra,
    Nextgroupplot,
    Closedcycle,
    Pgfplotsinvokeforeach,
    Pgfplotstableread,
    Colormaptodatafile,
    Colormaptoshadingspec,
    Tikzfillbetween,
    /// the fillbetween library's commands on soft paths
    Softpath,
    Pgfplotstabletypeset,
}

const COMMANDS: &[(&str, C)] = &[
    ("addplot", C::Addplot),
    ("addlegendentry", C::Addlegendentry),
    ("addlegendentryexpanded", C::Addlegendentryexpanded),
    ("legend", C::Legend),
    ("addlegendimage", C::Addlegendimage),
    ("pgfplotsextra", C::Pgfplotsextra),
    ("nextgroupplot", C::Nextgroupplot),
    ("closedcycle", C::Closedcycle),
    ("pgfplotsinvokeforeach", C::Pgfplotsinvokeforeach),
    ("pgfplotstableread", C::Pgfplotstableread),
    ("pgfplotscolormaptodatafile", C::Colormaptodatafile),
    ("pgfplotscolormaptoshadingspec", C::Colormaptoshadingspec),
    ("tikzfillbetween", C::Tikzfillbetween),
    ("pgfplotstabletypeset", C::Pgfplotstabletypeset),
    ("tikzgetnamedpath", C::Softpath),
    ("pgfplotspathaxisoutline", C::Softpath),
    ("pgfcomputeintersectionsegments", C::Softpath),
    ("pgfgetintersectionsegmentpath", C::Softpath),
    ("pgfsetpathandBB", C::Softpath),
    ("pgfaddpathandBB", C::Softpath),
    ("pgfpathreplacefirstmoveto", C::Softpath),
];

/// The environments that are axes.
pub fn is_axis(name: &str) -> bool {
    matches!(name, "axis" | "semilogxaxis" | "semilogyaxis" | "loglogaxis" | "polaraxis" | "smithchart" | "ternaryaxis")
}

/// Defines pgfplots' commands and keys.
pub fn define(e: &mut Engine) {
    for (i, (name, _)) in COMMANDS.iter().enumerate() {
        e.tex.define(name, Meaning::Native(BASE + i as u16));
    }
    keys::define(e);
}

pub fn is_command(id: u16) -> bool {
    (BASE..BASE + COMMANDS.len() as u16).contains(&id)
}

/// Commands a survey keeps for later (TikZ statements inside an axis).
pub fn is_deferred(name: &str) -> bool {
    matches!(name, "draw" | "fill" | "filldraw" | "path" | "node" | "coordinate" | "clip" | "shade" | "shadedraw" | "pattern" | "matrix" | "pic" | "useasboundingbox")
}

impl Engine {
    /// `\begin{axis}[options]`: the survey starts.
    pub fn begin_axis(&mut self, kind: &str) -> Result<(), Error> {
        let o = self.optional()?.unwrap_or_default();
        self.begin_axis_with(kind, o)
    }

    /// An axis with these options (`\nextgroupplot` makes them).
    pub fn begin_axis_with(&mut self, kind: &str, o: Vec<Token>) -> Result<(), Error> {
        if self.axis.is_some() {
            self.warn("nested axis environments are not supported (pgfplots refuses them too): the inner one is drawn on its own");
        }
        let opened = self.picture_depth == 0;
        if opened {
            self.begin_picture(None)?;
        }
        self.begin_group();
        dateplot::reset(self);
        let mut axis = Axis { kind: kind.to_string(), opened_picture: opened, depth: self.tex.group_depth(), ..Default::default() };
        let style = match kind {
            "polaraxis" => "enlarge x limits=false,every polar axis",
            "smithchart" => "every smithchart axis",
            "ternaryaxis" => {
                "enlargelimits=false,xmin=0,xmax=1,ymin=0,ymax=1,zmin=0,zmax=1,\
                 footnotesize/.append style={every axis title shift=15pt},tiny/.append style={every axis title shift=15pt},every ternary axis"
            }
            "semilogxaxis" => "every semilogx axis,xmode=log",
            "semilogyaxis" => "every semilogy axis,ymode=log",
            "loglogaxis" => "every loglog axis,xmode=log,ymode=log",
            _ => "every linear axis",
        };
        self.st.key_path = "/pgfplots".into();
        keys::run(self, "every axis/.try")?;
        keys::run(self, style)?;
        let saved = self.st.key_path.clone();
        axis.options = o.clone();
        // pgfplots' axis types add `data cs` after the options (`\pgfplotsdefineaxistype`)
        let o = if kind == "polaraxis" {
            let mut o = o;
            o.extend(toks(",data cs=polar"));
            o
        } else {
            o
        };
        keys::set(self, "/pgfplots/@axis type", if kind == "polaraxis" { "polar" } else { "cart" });
        self.process_keys(o, Some("/pgfplots")).map_err(|err| Error::Figure(format!("axis options: {err}")))?;
        if kind == "smithchart" {
            // the ticks by size, unless given
            let width = keys::length(self, "/pgfplots/width").unwrap_or(207.0 * crate::PT);
            keys::run(self, smith::size_style(width))?;
            let given = [keys::value(self, "/pgfplots/xtick").is_empty(), keys::value(self, "/pgfplots/ytick").is_empty()];
            if given[0] {
                keys::run(self, "default smithchart xtick")?;
            }
            if given[1] {
                keys::run(self, "default smithchart ytick")?;
            }
            if given[0] && given[1] {
                keys::run(self, "default smithchart xytick")?;
            }
            keys::set(self, "/pgfplots/is smithchart cs", "false");
        }
        if kind == "ternaryaxis" {
            // percentages: whole numbers, never scaled
            if keys::flag(self, "/pgfplots/ternary limits relative") {
                keys::run(self, "scaled ticks=false,ticklabel style={/pgf/number format/precision=0}")?;
            }
            for c in ['x', 'y', 'z'] {
                if keys::value(self, &format!("/pgfplots/{c} dir")) == "reverse" {
                    self.warn(format!("ternaryaxis: '{c} dir=reverse' not supported: drawn forward"));
                }
            }
        }
        self.st.key_path = saved;
        axis.numplots = 0;
        if let Some(code) = keys::value_tokens(self, "/pgfplots/execute at begin axis") {
            self.run_tokens(code)?;
        }
        self.axis = Some(Box::new(axis));
        Ok(())
    }

    /// `\end{axis}`: everything is drawn.
    pub fn end_axis(&mut self) -> Result<(), Error> {
        let Some(axis) = self.axis.take() else { return Ok(()) };
        if let Some(code) = keys::value_tokens(self, "/pgfplots/execute at end axis") {
            self.run_tokens(code)?;
        }
        let r = draw::draw_axis(self, &axis);
        // close the groups the body left open, then the axis' own
        while self.tex.group_depth() > axis.depth {
            self.end_group();
        }
        self.end_group();
        let r = match r {
            Ok(Some(cb)) => self.draw_colorbar(&cb),
            Ok(None) => Ok(()),
            Err(err) => Err(err),
        };
        if axis.opened_picture {
            self.end_picture();
        }
        r
    }

    /// pgfplots' commands.
    pub fn pgfplots_command(&mut self, id: u16) -> Result<(), Error> {
        let cmd = COMMANDS[(id - BASE) as usize].1;
        if self.axis.is_none()
            && !matches!(
                cmd,
                C::Pgfplotsinvokeforeach
                    | C::Closedcycle
                    | C::Nextgroupplot
                    | C::Pgfplotstableread
                    | C::Colormaptodatafile
                    | C::Colormaptoshadingspec
                    | C::Tikzfillbetween
                    | C::Softpath
                    | C::Pgfplotstabletypeset
            )
        {
            let rest = self.skip_statement()?;
            let name = COMMANDS[(id - BASE) as usize].0;
            self.warn(format!("\\{name} outside an axis: '{}' ignored", rest.chars().take(30).collect::<String>().trim()));
            return Ok(());
        }
        match cmd {
            C::Addplot => {
                let r = plot::addplot(self);
                if let Err(err) = r {
                    if super::is_limit(&err) {
                        return Err(err);
                    }
                    self.warn(format!("\\addplot: {err}; plot ignored"));
                    let _ = self.skip_statement();
                }
                Ok(())
            }
            C::Addlegendentry | C::Addlegendentryexpanded => {
                let options = self.optional()?.unwrap_or_default();
                let mut text = self.argument()?;
                if cmd == C::Addlegendentryexpanded {
                    text = self.tex.expand_fully(text)?;
                }
                if let Some(a) = self.axis.as_mut() {
                    a.legend.push(LegendEntry { options, text });
                }
                Ok(())
            }
            C::Legend => {
                let _ = self.optional()?;
                let list = self.argument()?;
                let entries = split_legend(&list);
                if let Some(a) = self.axis.as_mut() {
                    a.legend = entries;
                }
                Ok(())
            }
            C::Addlegendimage => {
                let o = self.argument()?;
                if let Some(a) = self.axis.as_mut() {
                    a.images.push(o);
                }
                Ok(())
            }
            C::Pgfplotsextra => {
                let code = self.argument()?;
                if let Some(a) = self.axis.as_mut() {
                    a.entries.push(Entry::Tikz(code));
                }
                Ok(())
            }
            C::Nextgroupplot => self.next_group_plot(),
            C::Closedcycle => Ok(()),
            C::Pgfplotstableread => plot::table_read(self),
            C::Pgfplotstabletypeset => plot::table_typeset(self),
            C::Colormaptodatafile => colormap::to_data_file(self),
            C::Colormaptoshadingspec => colormap::to_shading_spec(self),
            // `\tikzfillbetween[of=A and B, …]{style}`: the area between two named paths
            C::Tikzfillbetween => {
                let spec = self.optional()?.unwrap_or_default();
                let style = self.argument()?;
                let plot = Plot { style, ..Default::default() };
                self.begin_group();
                let r = fillbetween::draw(self, &plot, &spec);
                self.end_group();
                r
            }
            C::Softpath => fillbetween::soft_command(self, COMMANDS[(id - BASE) as usize].0),
            C::Pgfplotsinvokeforeach => {
                // \pgfplotsinvokeforeach{list}{code with #1}
                let list = self.argument()?;
                let body = self.argument()?;
                let list = self.tex.expand_fully(list)?;
                let parsed = tex::parse_body(body);
                for item in super::foreach_items(&list) {
                    let code = tex::substitute(&parsed, &[item]);
                    self.run_tokens(code)?;
                }
                Ok(())
            }
        }
    }

    /// A TikZ statement inside an axis, kept until the axis is drawn: the command and its tokens up
    /// to the `;`.
    pub fn defer_statement(&mut self, t: &Token) -> Result<(), Error> {
        let mut out = vec![t.clone()];
        let mut depth = 0usize;
        while let Some(x) = self.tex.get_token() {
            match x {
                Token::Char(_, Cat::Begin) => depth += 1,
                Token::Char(_, Cat::End) => {
                    if depth == 0 {
                        self.tex.back(x);
                        break;
                    }
                    depth -= 1;
                }
                _ => {}
            }
            let end = depth == 0 && x.is_char(';');
            out.push(x);
            if end || out.len() > tex::MAX_TOKENS {
                break;
            }
        }
        if let Some(a) = self.axis.as_mut() {
            a.entries.push(Entry::Tikz(out));
        }
        Ok(())
    }
}

/// `\legend{a,b,{c,d}}`: entries split at top-level commas (or `\\`).
fn split_legend(list: &[Token]) -> Vec<LegendEntry> {
    let backslashes = list.iter().any(|t| t.is_cs("\\"));
    let mut out = Vec::new();
    let mut cur = Vec::new();
    let mut depth = 0usize;
    for t in list {
        match t {
            Token::Char(_, Cat::Begin) => depth += 1,
            Token::Char(_, Cat::End) => depth = depth.saturating_sub(1),
            _ => {}
        }
        let sep = depth == 0 && if backslashes { t.is_cs("\\") } else { t.is_char(',') };
        if sep {
            out.push(std::mem::take(&mut cur));
            continue;
        }
        cur.push(t.clone());
    }
    if !crate::keys::trim(&cur).is_empty() || (!backslashes && !out.is_empty()) {
        out.push(cur);
    }
    out.into_iter()
        .map(|e| {
            let e = crate::keys::trim(&e).to_vec();
            let (options, text) = split_options(e);
            LegendEntry { options, text: tex::strip_outer_braces(text) }
        })
        .collect()
}

/// `[options]text` → (options, text).
pub fn split_options(v: Vec<Token>) -> (Vec<Token>, Vec<Token>) {
    if v.first().is_some_and(|t| t.is_char('[')) {
        let mut depth = 0usize;
        for (i, t) in v.iter().enumerate() {
            match t {
                Token::Char(_, Cat::Begin) => depth += 1,
                Token::Char(_, Cat::End) => depth = depth.saturating_sub(1),
                Token::Char(']', _) if depth == 0 => return (v[1..i].to_vec(), crate::keys::trim(&v[i + 1..]).to_vec()),
                _ => {}
            }
        }
    }
    (Vec::new(), v)
}

/// Tokens of `text`, read as TeX with `@` a letter.
pub fn toks(text: &str) -> Vec<Token> {
    tikra_tokens(text)
}