Skip to content
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

Logarithm axes in plots library #15

Closed
wants to merge 8 commits into from
Closed
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
113 changes: 102 additions & 11 deletions src/axes.typ
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,13 @@
#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) = (
min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, mode: mode,
)

// Format a tick value
#let format-tick-value(value, tic-options) = {
#let format-tick-value(value, tic-options, mode) = {
// Without it we get negative zero in conversion
// to content! Typst has negative zero floats.
if value == 0 { value = 0 }
Expand All @@ -246,6 +247,13 @@
$#round(value, digits)$
}

let format-log(value, base: 10) = {
let exponent = if value != 0 {
calc.log(calc.abs(value), base: base)
} else {0}
return $#base^#exponent$
}

let format-sci(value, digits) = {
let exponent = if value != 0 {
calc.floor(calc.log(calc.abs(value), base: 10))
Expand Down Expand Up @@ -277,6 +285,8 @@
value = (format)(value)
} else if format == "sci" {
value = format-sci(value, tic-options.at("decimals", default: 2))
} else if format == "log" {
value = format-log(value, base: tic-options.at("base", default: 10))
} else {
value = format-float(value, tic-options.at("decimals", default: 2))
}
Expand All @@ -303,6 +313,72 @@
return (v - min) / dt
}


#let compute-logarithmic-ticks(axis, style, add-zero: true) = {
let ferr = util.float-epsilon
let (min, max) = (axis.min, axis.max)
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(10, t / s), ticks, axis.mode),
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 n-minor = range(1, 10)

for t-minor in n-minor {

let place = t + calc.log(t-minor, base: 10)

if ( place > axis.max){ continue }
let v = (place / s - min) / dt

if v in major-tick-values {
// Prefer major ticks over minor ticks
continue
}
l.push((v, none, false))
}

}
}
}
}

return l
}

// Compute list of linear ticks for axis
//
// - axis (axis): Axis
Expand Down Expand Up @@ -334,7 +410,7 @@
if t / s == 0 and not add-zero { continue }

if v >= 0 - ferr and v <= 1 + ferr {
l.push((v, format-tick-value(t / s, ticks), true))
l.push((v, format-tick-value(t / s, ticks, axis.mode), true))
major-tick-values.push(v)
}
}
Expand Down Expand Up @@ -380,7 +456,7 @@
let (v, label) = (none, none)
if type(t) in (float, int) {
v = t
label = format-tick-value(t, axis.ticks)
label = format-tick-value(t, axis.ticks, axis.mode)
} else {
(v, label) = t
}
Expand Down Expand Up @@ -419,7 +495,8 @@
return step
}

if axis == none or axis.ticks == none { return () }
if axis == none { return () }
if axis.ticks == none { return () }
if axis.ticks.step == auto {
axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count)
}
Expand All @@ -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 @@ -480,10 +561,11 @@
size.at(1) -= y.inset.sum()

size = (rel: size, to: origin)
draw.set-viewport(origin, size,
bounds: (x.max - x.min,
y.max - y.min,
0))
draw.set-viewport(
origin,
size,
bounds: (x.max - x.min, y.max - y.min,0),
)
draw.translate((-x.min, -y.min))
body
})
Expand All @@ -499,6 +581,9 @@
// - dir (vector): Normalized grid direction vector along the grid axis
// - style (style): Axis style
#let draw-grid-lines(ctx, axis, ticks, low, high, dir, style) = {

// TODO: Account for log mode

let offset = (0,0)
if axis.inset != none {
let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v))
Expand Down Expand Up @@ -629,6 +714,7 @@
}

// Draw grid
// TODO: Account for log mode
group(name: "grid", ctx => {
let axes = (
("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom),
Expand All @@ -644,6 +730,7 @@

if not is-mirror {
on-layer(style.grid-layer, {
// TODO: Account for log mode
draw-grid-lines(ctx, axis, ticks, start, end, direction, style)
})
}
Expand Down Expand Up @@ -676,9 +763,13 @@
end = vector.add(end, padding)
}

// TODO: Account for log mode
let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end)

// TODO: Account for log mode
let path = _draw-axis-line(start, end, axis, is-horizontal, style)


on-layer(style.axis-layer, {
group(name: "axis", {
if draw-unset or axis != none {
Expand Down
1 change: 1 addition & 0 deletions src/plot.typ
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@
axis.min = util.min(axis.min, ..domain)
axis.max = util.max(axis.max, ..domain)
}
// axis.mode = if (axis.mode == none){"lin"}else{axis.mode}

axis-dict.at(name) = axis
}
Expand Down
22 changes: 20 additions & 2 deletions src/plot/line.typ
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,35 @@
#let _prepare(self, ctx) = {
let (x, y) = (ctx.x, ctx.y)

if "data" in self {
self.data = self.data.map( ((px, py))=>{
(
if x.mode == "log" {calc.log(calc.max(px, 0), base: 10)} else {px},
if y.mode == "log" {calc.log(calc.max(py, 0), base: 10)} else {py}
)
})
}

if "line-data" in self {
self.line-data = self.line-data.map( ((px, py))=>{
(
if x.mode == "log" {calc.log(calc.max(px, 0), base: 10)} else {px},
if y.mode == "log" {calc.log(calc.max(py, 0), base: 10)} else {py}
)
})
}

// Generate stroke paths
self.stroke-paths = util.compute-stroke-paths(self.line-data,
(x.min, y.min), (x.max, y.max))
(x.min, y.min), (x.max, y.max), (x.mode, y.mode))

// Compute fill paths if filling is requested
self.hypograph = self.at("hypograph", default: false)
self.epigraph = self.at("epigraph", default: false)
self.fill = self.at("fill", default: false)
if self.hypograph or self.epigraph or self.fill {
self.fill-paths = util.compute-fill-paths(self.line-data,
(x.min, y.min), (x.max, y.max))
(x.min, y.min), (x.max, y.max), (x.mode, y.mode))
}

return self
Expand Down
21 changes: 16 additions & 5 deletions src/plot/util.typ
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
/// - low (vector): Lower clip-window coordinate
/// - high (vector): Upper clip-window coordinate
/// -> array List of line-strips representing the paths insides the clip-window
#let clipped-paths(points, low, high, fill: false) = {
#let clipped-paths(points, low, high, fill: false, (log-x, log-y)) = {

let (min-x, max-x) = (calc.min(low.at(0), high.at(0)),
calc.max(low.at(0), high.at(0)))
let (min-y, max-y) = (calc.min(low.at(1), high.at(1)),
Expand Down Expand Up @@ -103,6 +104,13 @@
path.push(clamped-pt(prev))
}

// points = points.map(it=>{
// (
// if (log-x=="log") {calc.log(calc.max(it.first(),0.0000001))} else {it.first()},
// if (log-y=="log") {calc.log(calc.max(it.last(),0.0000001))} else {it.last()},
// )
// })

for i in range(1, points.len()) {
let prev = points.at(i - 1)
let pt = points.at(i)
Expand Down Expand Up @@ -176,8 +184,8 @@
/// - low (vector): Lower clip-window coordinate
/// - high (vector): Upper clip-window coordinate
/// -> array List of stroke paths
#let compute-stroke-paths(points, low, high) = {
clipped-paths(points, low, high, fill: false)
#let compute-stroke-paths(points, low, high, (log-x, log-y)) = {
clipped-paths(points, low, high, fill: false, (log-x, log-y))
}

/// Compute clipped fill path
Expand All @@ -186,8 +194,8 @@
/// - low (vector): Lower clip-window coordinate
/// - high (vector): Upper clip-window coordinate
/// -> array List of fill paths
#let compute-fill-paths(points, low, high) = {
clipped-paths(points, low, high, fill: true)
#let compute-fill-paths(points, low, high, (log-x, log-y)) = {
clipped-paths(points, low, high, fill: true, (log-x, log-y))
}

/// Return points of a sampled catmull-rom through the
Expand Down Expand Up @@ -307,6 +315,9 @@
axis.horizontal = get-axis-option(name, "horizontal",
get-default-axis-horizontal(name))

// Configure log mode
axis.mode = get-axis-option(name, "mode", "lin")

// Configure ticks
axis.ticks.list = get-axis-option(name, "ticks", ())
axis.ticks.step = get-axis-option(name, "tick-step", axis.ticks.step)
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.
35 changes: 35 additions & 0 deletions tests/axes/log-mode/test.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@


#set page(width: auto, height: auto)

#import "/tests/helper.typ": *
#import "/src/lib.typ": *
#import cetz: draw, canvas
#import cetz-plot: axes,

#box(stroke: 2pt + red, canvas({
import draw: *

plot.plot(
size: (9, 6),
axis-style: "scientific",
y-mode: "log",
y-grid: "both",
y-format: "log",
y-tick-step: 1,
y-minor-tick-step: 1,
y-min: 0, y-max: 3,

x-mode: "lin",
x-grid: "both",
// x-format: "log",
x-tick-step: 1,
x-minor-tick-step: 1,
x-min: 0.00001, x-max: 10,

{
plot.add(domain: (0.00001, 10), x => {calc.pow(10, x)}, mark: "o")
plot.add(domain: (0.00001, 10), x => {1+x})
}
)
}))
Binary file modified tests/plot/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.
Loading