diff --git a/gallery/line.png b/gallery/line.png index e3e1998..468aac5 100644 Binary files a/gallery/line.png and b/gallery/line.png differ diff --git a/gallery/line.typ b/gallery/line.typ index 702e127..f559a80 100644 --- a/gallery/line.typ +++ b/gallery/line.typ @@ -1,26 +1,39 @@ -#import "@preview/cetz:0.2.2": canvas +#import "@preview/cetz:0.2.2": canvas, draw #import "@preview/cetz-plot:0.1.0": plot #set page(width: auto, height: auto, margin: .5cm) #let style = (stroke: black, fill: rgb(0, 0, 200, 75)) -#canvas(length: 1cm, { - plot.plot(size: (8, 6), - x-tick-step: none, - x-ticks: ((-calc.pi, $-pi$), (0, $0$), (calc.pi, $pi$)), - y-tick-step: 1, +#let f1(x) = calc.sin(x) +#let fn = ( + ($ x - x^3"/"3! $, x => x - calc.pow(x, 3)/6), + ($ x - x^3"/"3! - x^5"/"5! $, x => x - calc.pow(x, 3)/6 + calc.pow(x, 5)/120), + ($ 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), +) + +#set text(size: 10pt) + +#canvas({ + import draw: * + + // Set-up a thin axis style + set-style(axes: (stroke: .5pt, tick: (stroke: .5pt)), + legend: (stroke: none, orientation: ttb, item: (spacing: .3), scale: 80%)) + + plot.plot(size: (12, 8), + x-tick-step: calc.pi/2, + x-format: plot.formats.multiple-of, + y-tick-step: 2, y-min: -2.5, y-max: 2.5, + legend: "inner-north", { - plot.add( - style: style, - domain: (-calc.pi, calc.pi), calc.sin) - plot.add( - hypograph: true, - style: style, - domain: (-calc.pi, calc.pi), calc.cos) - plot.add( - hypograph: true, - style: style, - domain: (-calc.pi, calc.pi), x => calc.cos(x + calc.pi)) + let domain = (-1.1 * calc.pi, +1.1 * calc.pi) + + for ((title, f)) in fn { + plot.add-fill-between(f, f1, domain: domain, + style: (stroke: none), label: title) + } + plot.add(f1, domain: domain, label: $ sin x $, + style: (stroke: black)) }) }) diff --git a/justfile b/justfile index 76ac86c..44c05c8 100644 --- a/justfile +++ b/justfile @@ -19,4 +19,4 @@ manual: typst c manual.typ manual.pdf gallery: - for f in "{{gallery_dir}}"/*.typ; do typst c "$f" "${f/typ/png}"; done + for f in "{{gallery_dir}}"/*.typ; do typst c --root . "$f" "${f/typ/png}"; done diff --git a/src/axes.typ b/src/axes.typ index 5bedf4d..cfd4669 100644 --- a/src/axes.typ +++ b/src/axes.typ @@ -1,4 +1,5 @@ #import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process +#import "/src/plot/formats.typ" #let typst-content = content @@ -250,27 +251,6 @@ $#round(value, digits)$ } - let format-sci(value, digits) = { - let exponent = if value != 0 { - calc.floor(calc.log(calc.abs(value), base: 10)) - } else { - 0 - } - - let ee = calc.pow(10, calc.abs(exponent + 1)) - if exponent > 0 { - value = value / ee * 10 - } else if exponent < 0 { - value = value * ee * 10 - } - - value = round(value, digits) - if exponent <= -1 or exponent >= 1 { - return $#value times 10^#exponent$ - } - return $#value$ - } - if type(value) != typst-content { let format = tic-options.at("format", default: "float") if format == none { @@ -280,8 +260,7 @@ } else if type(format) == function { value = (format)(value) } else if format == "sci" { - // Todo: Handle logarithmic including arbitrary base - value = format-sci(value, tic-options.at("decimals", default: 2)) + value = formats.sci(value, digits: tic-options.at("decimals", default: 2)) } else { value = format-float(value, tic-options.at("decimals", default: 2)) } @@ -381,7 +360,7 @@ #let compute-logarithmic-ticks(axis, style, add-zero: true) = { let ferr = util.float-epsilon let (min, max) = ( - calc.log(calc.max(axis.min, ferr), base: axis.base), + calc.log(calc.max(axis.min, ferr), base: axis.base), calc.log(calc.max(axis.max, ferr), base: axis.base) ) let dt = max - min; if (dt == 0) { dt = 1 } @@ -439,11 +418,11 @@ } } - + } } } - + return l } diff --git a/src/plot.typ b/src/plot.typ index 9c9cd6a..a30587e 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -12,6 +12,7 @@ #import "/src/plot/bar.typ": add-bar #import "/src/plot/errorbar.typ": add-errorbar #import "/src/plot/mark.typ" +#import "/src/plot/formats.typ" #import plot-legend: add-legend #let default-colors = (blue, red, green, yellow, black) diff --git a/src/plot/formats.typ b/src/plot/formats.typ new file mode 100644 index 0000000..635f256 --- /dev/null +++ b/src/plot/formats.typ @@ -0,0 +1,119 @@ +// Compare two floats +#let _compare(a, b, eps: 1e-6) = { + return calc.abs(a - b) <= eps +} + +// Pre-computed table of fractions +#let _common-denoms = range(2, 11 + 1).map(d => { + (d, range(1, d).map(n => n/d)) +}) + +#let _find-fraction(v, denom: auto, eps: 1e-6) = { + let i = calc.floor(v) + let f = v - i + if _compare(f, 0, eps: eps) { + return $#v$ + } + + let denom = if denom != auto { + for n in range(1, denom) { + if _compare(f, n/denom, eps: eps) { + denom + } + } + } else { + (() => { + for ((denom, tab)) in _common-denoms { + for vv in tab { + if _compare(f, vv, eps: eps) { + return denom + } + } + } + })() + } + + if denom != none { + return if v < 0 { $-$ } else {} + $#calc.round(calc.abs(v) * denom)/#denom$ + } +} + +/// Fraction tick formatter +/// +/// - value (number): Value to format +/// - denom (auto, int): Denominator for result fractions. If set to `auto`, +/// a hardcoded fraction table is used for finding fractions with a +/// denominator <= 11. +/// - eps (number): Epsilon used for comparison +/// -> Content if a matching fraction could be found or none +#let fraction(value, denom: auto, eps: 1e-6) = { + return _find-fraction(value, denom: denom, eps: eps) +} + +/// Multiple of tick formatter +/// +/// ```example +/// plot.plot(x-format: plot.formats.multiple-of, +/// x-tick-step: calc.pi/4, { +/// plot.add(calc.sin, domain: (-calc.pi, 1.5 * calc.pi)) +/// }) +/// ``` +/// +/// - value (number): Value to format +/// - factor (number): Factor value is expected to be a multiple of. +/// - symbol (content): Suffix symbol. For `value` = 0, the symbol is not +/// appended. +/// - fraction (none, true, int): If not none, try finding matching fractions +/// using the same mechanism as `fraction`. If set to an integer, that integer +/// is used as denominator. If set to `none` or `false`, or if no fraction +/// could be found, a real number with `digits` digits is used. +/// - digits (int): Number of digits to use for rounding +/// - eps (number): Epsilon used for comparison +/// -> Content if a matching fraction could be found or none +#let multiple-of(value, factor: calc.pi, symbol: $pi$, fraction: true, digits: 2, eps: 1e-6) = { + if _compare(value, 0, eps: eps) { + return $0$ + } + + let a = value / factor + if _compare(a, 1, eps: eps) { + return symbol + } else if _compare(a, -1, eps: eps) { + return $-$ + symbol + } + + if fraction != none { + let frac = _find-fraction(a, denom: if fraction == true { auto } else { fraction }) + if frac != none { + return frac + symbol + } + } + + return $#calc.round(a, digits: digits)$ + symbol +} + +/// Scientific notation tick formatter +/// +/// - value (number): Value to format +/// - digits (int): Number of digits for rouding the factor +/// -> Content +#let sci(value, digits: 2) = { + let exponent = if value != 0 { + calc.floor(calc.log(calc.abs(value), base: 10)) + } else { + 0 + } + + let ee = calc.pow(10, calc.abs(exponent + 1)) + if exponent > 0 { + value = value / ee * 10 + } else if exponent < 0 { + value = value * ee * 10 + } + + value = calc.round(value, digits: digits) + if exponent <= -1 or exponent >= 1 { + return $#value times 10^#exponent$ + } + return $#value$ +} diff --git a/src/plot/legend.typ b/src/plot/legend.typ index 074f75e..f26a43b 100644 --- a/src/plot/legend.typ +++ b/src/plot/legend.typ @@ -23,6 +23,7 @@ ) ), radius: 0, + scale: 100%, ) // Map position to legend group anchor @@ -117,6 +118,9 @@ assert(style.orientation in (ttb, ltr), message: "Unsupported legend orientation.") + // Scaling + draw.scale(style.scale) + // Position let position = if position == auto { style.default-position diff --git a/tests/plot/format/ref/1.png b/tests/plot/format/ref/1.png new file mode 100644 index 0000000..b46e922 Binary files /dev/null and b/tests/plot/format/ref/1.png differ diff --git a/tests/plot/format/test.typ b/tests/plot/format/test.typ new file mode 100644 index 0000000..53abf75 --- /dev/null +++ b/tests/plot/format/test.typ @@ -0,0 +1,39 @@ +#set page(width: auto, height: auto) +#import "/tests/helper.typ": * +#import cetz: draw +#import cetz-plot: plot + +#let data = ((-calc.pi, -1), (+calc.pi, +1)) + +#test-case({ + plot.plot( + size: (8, 4), + x-min: -2 * calc.pi, + x-max: +2 * calc.pi, + x-tick-step: calc.pi/2, + x-format: plot.formats.multiple-of, { + plot.add(data) + }) +}) + +#test-case({ + plot.plot( + size: (8, 4), + x-min: -2, + x-max: +2, + x-tick-step: 1/3, + x-format: plot.formats.fraction, { + plot.add(data) + }) +}) + +#test-case({ + plot.plot( + size: (8, 4), + x-min: -2, + x-max: +2, + x-tick-step: 1/3, + x-format: plot.formats.fraction.with(denom: 33), { + plot.add(data) + }) +})