diff --git a/src/axes.typ b/src/axes.typ index 04d404b..5bedf4d 100644 --- a/src/axes.typ +++ b/src/axes.typ @@ -225,11 +225,15 @@ // - unit (content): Tick label suffix // - decimals (int): Tick float decimal length // - label (content): Axis label +// - mode (string): Axis scaling function. Takes `lin` or `log` +// - base (number): Base for tick labels when logarithmically scaled. #let axis(min: -1, max: 1, label: none, ticks: (step: auto, minor-step: none, unit: none, decimals: 2, grid: false, - format: "float")) = ( - min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, + format: "float" + ), + mode: auto, base: auto) = ( + min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, mode: mode, base: base ) // Format a tick value @@ -276,6 +280,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)) } else { value = format-float(value, tic-options.at("decimals", default: 2)) @@ -370,6 +375,78 @@ return l } +// Compute list of linear ticks for axis +// +// - axis (axis): Axis +#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.max, ferr), base: axis.base) + ) + let dt = max - min; if (dt == 0) { dt = 1 } + let ticks = axis.ticks + + let tick-limit = style.tick-limit + let minor-tick-limit = style.minor-tick-limit + let l = () + + if ticks != none { + let major-tick-values = () + if "step" in ticks and ticks.step != none { + assert(ticks.step >= 0, + message: "Axis tick step must be positive and non 0.") + if axis.min > axis.max { ticks.step *= -1 } + + let s = 1 / ticks.step + + let num-ticks = int(max * s + 1.5) - int(min * s) + assert(num-ticks <= tick-limit, + message: "Number of major ticks exceeds limit " + str(tick-limit)) + + let n = range( + int(min * s), + int(max * s + 1.5) + ) + + for t in n { + let v = (t / s - min) / dt + if t / s == 0 and not add-zero { continue } + + if v >= 0 - ferr and v <= 1 + ferr { + l.push((v, format-tick-value( calc.pow(axis.base, t / s), ticks), true)) + major-tick-values.push(v) + } + } + } + + if "minor-step" in ticks and ticks.minor-step != none { + assert(ticks.minor-step >= 0, + message: "Axis minor tick step must be positive") + if axis.min > axis.max { ticks.minor-step *= -1 } + + let s = 1 / ticks.step + let n = range(int(min * s)-1, int(max * s + 1.5)+1) + + for t in n { + for vv in range(1, int(axis.base / ticks.minor-step)) { + + let v = ( (calc.log(vv * ticks.minor-step, base: axis.base) + t)/ s - min) / dt + if v in major-tick-values {continue} + + if v != none and v >= 0 and v <= 1 + ferr { + l.push((v, none, false)) + } + + } + + } + } + } + + return l +} + // Get list of fixed axis ticks // // - axis (axis): Axis object @@ -431,7 +508,11 @@ } } - let ticks = compute-linear-ticks(axis, style, add-zero: add-zero) + let ticks = if axis.mode == "log" { + compute-logarithmic-ticks(axis, style, add-zero: add-zero) + } else { + compute-linear-ticks(axis, style, add-zero: add-zero) + } ticks += fixed-ticks(axis) return ticks } @@ -471,35 +552,23 @@ // - vec (vector): Input vector to transform // -> vector #let transform-vec(size, x-axis, y-axis, z-axis, vec) = { - let (ox, oy, ..) = (0, 0, 0) - ox += x-axis.inset.at(0) - oy += y-axis.inset.at(0) - - let (sx, sy) = size - sx -= x-axis.inset.sum() - sy -= y-axis.inset.sum() - - let x-range = x-axis.max - x-axis.min - let y-range = y-axis.max - y-axis.min - let z-range = 0 //z-axis.max - z-axis.min - - let fx = sx / x-range - let fy = sy / y-range - let fz = 0 //sz / z-range - - let x-low = calc.min(x-axis.min, x-axis.max) - let x-high = calc.max(x-axis.min, x-axis.max) - let y-low = calc.min(y-axis.min, y-axis.max) - let y-high = calc.max(y-axis.min, y-axis.max) - //let z-low = calc.min(z-axis.min, z-axis.max) - //let z-hihg = calc.max(z-axis.min, z-axis.max) - - let (x, y, ..) = vec - - return ( - (x - x-axis.min) * fx + ox, - (y - y-axis.min) * fy + oy, - 0) //(z - z-axis.min) * fz + oz) + + let (x,y,) = for (dim, axis) in (x-axis, y-axis).enumerate() { + + let s = size.at(dim) - axis.inset.sum() + let o = axis.inset.at(0) + + let transform-func(n) = if (axis.mode == "log") { + calc.log(calc.max(n, util.float-epsilon), base: axis.base) + } else {n} + + let range = transform-func(axis.max) - transform-func(axis.min) + + let f = s / range + ((transform-func(vec.at(dim)) - transform-func(axis.min)) * f + o,) + } + + return (x, y, 0) } // Draw inside viewport coordinates of two axes diff --git a/src/plot.typ b/src/plot.typ index 8a062dc..9c9cd6a 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -117,6 +117,11 @@ /// #show-parameter-block("unit", ("none", "content"), default: "none", [ /// Suffix to append to all tick labels. /// ]) +/// #show-parameter-block("mode", ("none", "string"), default: "none", [ +/// The scaling function of the axis. Takes `lin` (default) for linear scaling, +/// and `log` for logarithmic scaling.]) +/// #show-parameter-block("base", ("none", "number"), default: "none", [ +/// The base to be used when labeling axis ticks in logarithmic scaling]) /// #show-parameter-block("grid", ("bool", "string"), default: "false", [ /// If `true` or `"major"`, show grid lines for all major ticks. If set /// to `"minor"`, show grid lines for minor ticks only. diff --git a/src/plot/annotation.typ b/src/plot/annotation.typ index a8584aa..19fb2e5 100644 --- a/src/plot/annotation.typ +++ b/src/plot/annotation.typ @@ -1,5 +1,5 @@ -#import "/src/cetz.typ": draw, process, util, matrix - +#import "/src/cetz.typ" +#import cetz: draw, process, util, matrix #import "util.typ" #import "sample.typ" @@ -41,7 +41,7 @@ axes: axes, resize: resize, background: background, - padding: util.as-padding-dict(padding), + padding: cetz.util.as-padding-dict(padding), ),) } diff --git a/src/plot/util.typ b/src/plot/util.typ index 13a9e59..bff54fc 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -303,6 +303,9 @@ axis.min -= 1; axis.max += 1 } + axis.mode = get-axis-option(name, "mode", "lin") + axis.base = get-axis-option(name, "base", 10) + // Configure axis orientation axis.horizontal = get-axis-option(name, "horizontal", get-default-axis-horizontal(name)) diff --git a/tests/axes/log-mode/ref/1.png b/tests/axes/log-mode/ref/1.png new file mode 100644 index 0000000..388b6a4 Binary files /dev/null and b/tests/axes/log-mode/ref/1.png differ diff --git a/tests/axes/log-mode/test.typ b/tests/axes/log-mode/test.typ new file mode 100644 index 0000000..f732959 --- /dev/null +++ b/tests/axes/log-mode/test.typ @@ -0,0 +1,166 @@ + + +#set page(width: auto, height: auto) + +#import "/tests/helper.typ": * +#import "/src/lib.typ": * +#import cetz: draw, canvas +#import cetz-plot: axes, + +// plot.add test with logarithmic scaling +#box(stroke: 2pt + red, canvas({ + import draw: * + + plot.plot( + size: (9, 6), + axis-style: "scientific", + y-mode: "log", y-base: 10, + y-format: "sci", + x-min: 1, x-max: 10, x-tick-step: 1, + y-min: 1, y-max: 10000, y-tick-step: 1, y-minor-tick-step: 1, + x-grid: "both", + y-grid: "both", + { + plot.add( + domain: (0, 10), + x => {calc.pow(10, x)}, + samples: 100, + line: "raw", + label: $y=10^x$ + ) + plot.add( + domain: (1, 10), + x => {x}, + samples: 100, + line: "raw", + hypograph: true, + label: $y=x$ + ) + } + ) +})) + +// Bode plot test +#box(stroke: 2pt + red,{ + canvas({ + import draw: * + cetz.draw.set-style( + grid: (stroke: (paint: luma(83.33%), thickness: 1pt, dash: "dotted")), + minor-grid: (stroke: (paint: luma(83.33%), thickness: 0.5pt, dash: "dotted")), + ) + plot.plot( + size: (16, 6), + axis-style: "scientific", + x-format: none, x-label: none, + x-mode: "log", + x-min: 0.01, x-max: 100, x-tick-step: 1, x-minor-tick-step: 1, + y-label: [Magnitude ($upright(d B)$)], + y-min: -40, y-max: 10, y-tick-step: 10, + x-grid: "both", + y-grid: "both", + { + plot.add(domain: (0.01, 100), x => {0}) + } + ) + }) + canvas({ + import draw: * + cetz.draw.set-style( + grid: (stroke: (paint: luma(83.33%), thickness: 1pt, dash: "dotted")), + minor-grid: (stroke: (paint: luma(83.33%), thickness: 0.5pt, dash: "dotted")), + ) + plot.plot( + size: (16, 6), + axis-style: "scientific", + x-mode: "log", + x-min: 0.01, x-max: 100, x-tick-step: 1, x-minor-tick-step: 1, + x-label: [Frequency ($upright(r a d)\/s$)], + y-label: [Phase ($upright(d e g)$)], + y-min: -90, y-max: 0, y-tick-step: 45, + x-grid: "both", + y-grid: "both", + { + plot.add(domain: (0.01, 100), x => {-40}) + } + ) + }) +}) + +// Column chart test +#box(stroke: 2pt + red, canvas({ + import draw: * + + plot.plot( + size: (9, 6), + axis-style: "scientific", + y-mode: "log", y-base: 10, + y-format: "sci", + x-min: -0.5, x-max: 4.5, x-tick-step: 1, + y-min: 0.1, y-max: 10000, y-tick-step: 1, y-minor-tick-step: 1, + x-grid: "both", + y-grid: "both", + { + plot.add-bar( + (1, 10, 100, 1000, 10000).enumerate().map(((x,y))=>{(x,y)}), + bar-width: 0.8, + ) + } + ) +})) + +// Scatter plot test +#box(stroke: 2pt + red, canvas({ + import draw: * + + plot.plot( + size: (9, 6), + axis-style: "scientific", + y-mode: "log", y-base: 100, + y-format: "sci", + x-min: -0.5, x-max: 4.5, x-tick-step: 1, + y-min: 0.1, y-max: 10000, y-tick-step: 1, y-minor-tick-step: 10, + x-grid: "both", + y-grid: "both", + { + plot.add( + ((0, 1),(1,2),(1,3),(2, 100),(2,150),(3, 1000),), + style: (stroke: none), + mark: "o" + ) + plot.annotate({ + rect((0, 1), (calc.pi, 10), fill: rgb(50,50,200,50)) + content((2, 3), [Annotation]) + }) + plot.annotate({ + rect((0, 1000), (calc.pi, 10000), fill: rgb(50,50,200,50)) + content((2, 3000), [Annotation]) + }) + } + ) +})) + +// Box plot test +#box(stroke: 2pt + red, canvas({ + import draw: * + + plot.plot( + size: (9, 6), + axis-style: "scientific", + y-mode: "log", y-base: 10, + y-format: "sci", + x-min: -0.5, x-max: 2.5, x-tick-step: 1, + y-min: 0.1, y-max: 15000, y-tick-step: 1, y-minor-tick-step: 1, + x-grid: "both", + y-grid: "both", + { + plot.add-boxwhisker( + ( + (x: 0, min: 1, q1: 10, q2: 100, q3: 1000, max: 10000), + (x: 1, min: 100, q1: 200, q2: 300, q3: 400, max: 500), + (x: 2, min: 10, q1: 100, q2: 500, q3: 1000, max: 5000), + ), + ) + } + ) +})) + diff --git a/tests/plot/marks/test.typ b/tests/plot/marks/test.typ index 80ff318..7daba6c 100644 --- a/tests/plot/marks/test.typ +++ b/tests/plot/marks/test.typ @@ -11,14 +11,10 @@ plot.plot( size: (5,5), - x-min: 0, - x-max: 1, - y-min: 0, - y-max: 1, - x2-min: 1, - x2-max: 0, - y2-min: 1, - y2-max: 0, + x-min: 0, x-max: 1, + y-min: 0, y-max: 1, + x2-min: 1, x2-max: 0, + y2-min: 1, y2-max: 0, for axes in axis-options { plot.add( axes: axes, diff --git a/tests/plot/ref/1.png b/tests/plot/ref/1.png index f3e3a1c..81e2422 100644 Binary files a/tests/plot/ref/1.png and b/tests/plot/ref/1.png differ