Skip to content

Commit c2a6341

Browse files
committed
plot: Add predefined tick formatters
1 parent ee91ee2 commit c2a6341

File tree

9 files changed

+199
-44
lines changed

9 files changed

+199
-44
lines changed

gallery/line.png

1.53 KB
Loading

gallery/line.typ

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
1-
#import "@preview/cetz:0.2.2": canvas
1+
#import "@preview/cetz:0.2.2": canvas, draw
22
#import "@preview/cetz-plot:0.1.0": plot
33

44
#set page(width: auto, height: auto, margin: .5cm)
55

66
#let style = (stroke: black, fill: rgb(0, 0, 200, 75))
77

8-
#canvas(length: 1cm, {
9-
plot.plot(size: (8, 6),
10-
x-tick-step: none,
11-
x-ticks: ((-calc.pi, $-pi$), (0, $0$), (calc.pi, $pi$)),
12-
y-tick-step: 1,
8+
#let f1(x) = calc.sin(x)
9+
#let fn = (
10+
($ x - x^3"/"3! $, x => x - calc.pow(x, 3)/6),
11+
($ x - x^3"/"3! - x^5"/"5! $, x => x - calc.pow(x, 3)/6 + calc.pow(x, 5)/120),
12+
($ x - x^3"/"3! - x^5"/"5! - x^7"/"7! $, x => x - calc.pow(x, 3)/6 + calc.pow(x, 5)/120 - calc.pow(x, 7)/5040),
13+
)
14+
15+
#set text(size: 10pt)
16+
17+
#canvas({
18+
import draw: *
19+
20+
// Set-up a thin axis style
21+
set-style(axes: (stroke: .5pt, tick: (stroke: .5pt)),
22+
legend: (stroke: none, orientation: ttb, item: (spacing: .3), scale: 80%))
23+
24+
plot.plot(size: (12, 8),
25+
x-tick-step: calc.pi/2,
26+
x-format: plot.formats.multiple-of,
27+
y-tick-step: 2, y-min: -2.5, y-max: 2.5,
28+
legend: "inner-north",
1329
{
14-
plot.add(
15-
style: style,
16-
domain: (-calc.pi, calc.pi), calc.sin)
17-
plot.add(
18-
hypograph: true,
19-
style: style,
20-
domain: (-calc.pi, calc.pi), calc.cos)
21-
plot.add(
22-
hypograph: true,
23-
style: style,
24-
domain: (-calc.pi, calc.pi), x => calc.cos(x + calc.pi))
30+
let domain = (-1.1 * calc.pi, +1.1 * calc.pi)
31+
32+
for ((title, f)) in fn {
33+
plot.add-fill-between(f, f1, domain: domain,
34+
style: (stroke: none), label: title)
35+
}
36+
plot.add(f1, domain: domain, label: $ sin x $,
37+
style: (stroke: black))
2538
})
2639
})

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ manual:
1919
typst c manual.typ manual.pdf
2020

2121
gallery:
22-
for f in "{{gallery_dir}}"/*.typ; do typst c "$f" "${f/typ/png}"; done
22+
for f in "{{gallery_dir}}"/*.typ; do typst c --root . "$f" "${f/typ/png}"; done

src/axes.typ

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process
2+
#import "/src/plot/formats.typ"
23

34
#let typst-content = content
45

@@ -250,27 +251,6 @@
250251
$#round(value, digits)$
251252
}
252253

253-
let format-sci(value, digits) = {
254-
let exponent = if value != 0 {
255-
calc.floor(calc.log(calc.abs(value), base: 10))
256-
} else {
257-
0
258-
}
259-
260-
let ee = calc.pow(10, calc.abs(exponent + 1))
261-
if exponent > 0 {
262-
value = value / ee * 10
263-
} else if exponent < 0 {
264-
value = value * ee * 10
265-
}
266-
267-
value = round(value, digits)
268-
if exponent <= -1 or exponent >= 1 {
269-
return $#value times 10^#exponent$
270-
}
271-
return $#value$
272-
}
273-
274254
if type(value) != typst-content {
275255
let format = tic-options.at("format", default: "float")
276256
if format == none {
@@ -280,8 +260,7 @@
280260
} else if type(format) == function {
281261
value = (format)(value)
282262
} else if format == "sci" {
283-
// Todo: Handle logarithmic including arbitrary base
284-
value = format-sci(value, tic-options.at("decimals", default: 2))
263+
value = formats.sci(value, digits: tic-options.at("decimals", default: 2))
285264
} else {
286265
value = format-float(value, tic-options.at("decimals", default: 2))
287266
}
@@ -381,7 +360,7 @@
381360
#let compute-logarithmic-ticks(axis, style, add-zero: true) = {
382361
let ferr = util.float-epsilon
383362
let (min, max) = (
384-
calc.log(calc.max(axis.min, ferr), base: axis.base),
363+
calc.log(calc.max(axis.min, ferr), base: axis.base),
385364
calc.log(calc.max(axis.max, ferr), base: axis.base)
386365
)
387366
let dt = max - min; if (dt == 0) { dt = 1 }
@@ -439,11 +418,11 @@
439418
}
440419

441420
}
442-
421+
443422
}
444423
}
445424
}
446-
425+
447426
return l
448427
}
449428

src/plot.typ

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#import "/src/plot/bar.typ": add-bar
1313
#import "/src/plot/errorbar.typ": add-errorbar
1414
#import "/src/plot/mark.typ"
15+
#import "/src/plot/formats.typ"
1516
#import plot-legend: add-legend
1617

1718
#let default-colors = (blue, red, green, yellow, black)

src/plot/formats.typ

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Compare two floats
2+
#let _compare(a, b, eps: 1e-6) = {
3+
return calc.abs(a - b) <= eps
4+
}
5+
6+
// Pre-computed table of fractions
7+
#let _common-denoms = range(2, 11 + 1).map(d => {
8+
(d, range(1, d).map(n => n/d))
9+
})
10+
11+
#let _find-fraction(v, denom: auto, eps: 1e-6) = {
12+
let i = calc.floor(v)
13+
let f = v - i
14+
if _compare(f, 0, eps: eps) {
15+
return $#v$
16+
}
17+
18+
let denom = if denom != auto {
19+
for n in range(1, denom) {
20+
if _compare(f, n/denom, eps: eps) {
21+
denom
22+
}
23+
}
24+
} else {
25+
(() => {
26+
for ((denom, tab)) in _common-denoms {
27+
for vv in tab {
28+
if _compare(f, vv, eps: eps) {
29+
return denom
30+
}
31+
}
32+
}
33+
})()
34+
}
35+
36+
if denom != none {
37+
return if v < 0 { $-$ } else {} + $#calc.round(calc.abs(v) * denom)/#denom$
38+
}
39+
}
40+
41+
/// Fraction tick formatter
42+
///
43+
/// - value (number): Value to format
44+
/// - denom (auto, int): Denominator for result fractions. If set to `auto`,
45+
/// a hardcoded fraction table is used for finding fractions with a
46+
/// denominator <= 11.
47+
/// - eps (number): Epsilon used for comparison
48+
/// -> Content if a matching fraction could be found or none
49+
#let fraction(value, denom: auto, eps: 1e-6) = {
50+
return _find-fraction(value, denom: denom, eps: eps)
51+
}
52+
53+
/// Multiple of tick formatter
54+
///
55+
/// ```example
56+
/// plot.plot(x-format: plot.formats.multiple-of,
57+
/// x-tick-step: calc.pi/4, {
58+
/// plot.add(calc.sin, domain: (-calc.pi, 1.5 * calc.pi))
59+
/// })
60+
/// ```
61+
///
62+
/// - value (number): Value to format
63+
/// - factor (number): Factor value is expected to be a multiple of.
64+
/// - symbol (content): Suffix symbol. For `value` = 0, the symbol is not
65+
/// appended.
66+
/// - fraction (none, true, int): If not none, try finding matching fractions
67+
/// using the same mechanism as `fraction`. If set to an integer, that integer
68+
/// is used as denominator. If set to `none` or `false`, or if no fraction
69+
/// could be found, a real number with `digits` digits is used.
70+
/// - digits (int): Number of digits to use for rounding
71+
/// - eps (number): Epsilon used for comparison
72+
/// -> Content if a matching fraction could be found or none
73+
#let multiple-of(value, factor: calc.pi, symbol: $pi$, fraction: true, digits: 2, eps: 1e-6) = {
74+
if _compare(value, 0, eps: eps) {
75+
return $0$
76+
}
77+
78+
let a = value / factor
79+
if _compare(a, 1, eps: eps) {
80+
return symbol
81+
} else if _compare(a, -1, eps: eps) {
82+
return $-$ + symbol
83+
}
84+
85+
if fraction != none {
86+
let frac = _find-fraction(a, denom: if fraction == true { auto } else { fraction })
87+
if frac != none {
88+
return frac + symbol
89+
}
90+
}
91+
92+
return $#calc.round(a, digits: digits)$ + symbol
93+
}
94+
95+
/// Scientific notation tick formatter
96+
///
97+
/// - value (number): Value to format
98+
/// - digits (int): Number of digits for rouding the factor
99+
/// -> Content
100+
#let sci(value, digits: 2) = {
101+
let exponent = if value != 0 {
102+
calc.floor(calc.log(calc.abs(value), base: 10))
103+
} else {
104+
0
105+
}
106+
107+
let ee = calc.pow(10, calc.abs(exponent + 1))
108+
if exponent > 0 {
109+
value = value / ee * 10
110+
} else if exponent < 0 {
111+
value = value * ee * 10
112+
}
113+
114+
value = calc.round(value, digits: digits)
115+
if exponent <= -1 or exponent >= 1 {
116+
return $#value times 10^#exponent$
117+
}
118+
return $#value$
119+
}

src/plot/legend.typ

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
),
2525
radius: 0,
26+
scale: 100%,
2627
)
2728

2829
// Map position to legend group anchor
@@ -117,6 +118,9 @@
117118
assert(style.orientation in (ttb, ltr),
118119
message: "Unsupported legend orientation.")
119120

121+
// Scaling
122+
draw.scale(style.scale)
123+
120124
// Position
121125
let position = if position == auto {
122126
style.default-position

tests/plot/format/ref/1.png

42 KB
Loading

tests/plot/format/test.typ

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#set page(width: auto, height: auto)
2+
#import "/tests/helper.typ": *
3+
#import cetz: draw
4+
#import cetz-plot: plot
5+
6+
#let data = ((-calc.pi, -1), (+calc.pi, +1))
7+
8+
#test-case({
9+
plot.plot(
10+
size: (8, 4),
11+
x-min: -2 * calc.pi,
12+
x-max: +2 * calc.pi,
13+
x-tick-step: calc.pi/2,
14+
x-format: plot.formats.multiple-of, {
15+
plot.add(data)
16+
})
17+
})
18+
19+
#test-case({
20+
plot.plot(
21+
size: (8, 4),
22+
x-min: -2,
23+
x-max: +2,
24+
x-tick-step: 1/3,
25+
x-format: plot.formats.fraction, {
26+
plot.add(data)
27+
})
28+
})
29+
30+
#test-case({
31+
plot.plot(
32+
size: (8, 4),
33+
x-min: -2,
34+
x-max: +2,
35+
x-tick-step: 1/3,
36+
x-format: plot.formats.fraction.with(denom: 33), {
37+
plot.add(data)
38+
})
39+
})

0 commit comments

Comments
 (0)