Skip to content

Logarithm axes in plots library #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 104 additions & 34 deletions src/axes.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -471,39 +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,y,) = for (dim, axis) in (x-axis, y-axis).enumerate() {

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 s = size.at(dim) - axis.inset.sum()
let o = axis.inset.at(0)

let fx = sx / x-range
let fy = sy / y-range
let fz = 0 //sz / z-range
let transform-func(n) = if (axis.mode == "log") {
calc.log(calc.max(n, util.float-epsilon), base: axis.base)
} else {n}

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 range = transform-func(axis.max) - transform-func(axis.min)

let (x, y, ..) = vec

if x < x-low or x > x-high or y < y-low or x > x-high {
return none
let f = s / range
((transform-func(vec.at(dim)) - transform-func(axis.min)) * f + o,)
}

return (
(x - x-axis.min) * fx + ox,
(y - y-axis.min) * fy + oy,
0) //(z - z-axis.min) * fz + oz)
return (x, y, 0)
}

// Draw inside viewport coordinates of two axes
Expand All @@ -523,11 +588,16 @@
ctx.transform = transform

drawables = drawables.map(d => {
d.segments = d.segments.map(((kind, ..pts)) => {
(kind, ..pts.map(pt => {
transform-vec(size, x, y, none, pt)
}))
})
if "segments" in d {
d.segments = d.segments.map(((kind, ..pts)) => {
(kind, ..pts.map(pt => {
transform-vec(size, x, y, none, pt)
}))
})
}
if "pos" in d {
d.pos = transform-vec(size, x, y, none, d.pos)
}
return d
})

Expand Down
5 changes: 5 additions & 0 deletions src/plot.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions src/plot/annotation.typ
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -41,7 +41,7 @@
axes: axes,
resize: resize,
background: background,
padding: util.as-padding-dict(padding),
padding: cetz.util.as-padding-dict(padding),
),)
}

Expand Down
3 changes: 3 additions & 0 deletions src/plot/util.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this could be useful for other things, like the ticks PR you have at the moment #18

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we merge #18, you could pick the default formatter based on the axis options 👍.
The formatter itself does not know about the axis.


// Configure axis orientation
axis.horizontal = get-axis-option(name, "horizontal",
get-default-axis-horizontal(name))
Expand Down
Binary file added tests/axes/log-mode/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
166 changes: 166 additions & 0 deletions tests/axes/log-mode/test.typ
Original file line number Diff line number Diff line change
@@ -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),
),
)
}
)
}))

Loading
Loading