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

Polar plot through scientific-polar axis style #26

Closed
wants to merge 10 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
957 changes: 99 additions & 858 deletions src/axes.typ

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions src/axes/draw.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#import "/src/cetz.typ": draw, util, vector

#let _inset-axis-points(ctx, style, axis, start, end) = {
if axis == none { return (start, end) }

let (low, high) = axis.inset.map(v => util.resolve-number(ctx, v))

let is-horizontal = start.at(1) == end.at(1)
if is-horizontal {
start = vector.add(start, (low, 0))
end = vector.sub(end, (high, 0))
} else {
start = vector.add(start, (0, low))
end = vector.sub(end, (0, high))
}
return (start, end)
}

#let _draw-axis-line(start, end, axis, is-horizontal, style) = {
let enabled = if axis != none and axis.show-break {
axis.min > 0 or axis.max < 0
} else { false }

if enabled {
let size = if is-horizontal {
(style.break-point.width, 0)
} else {
(0, style.break-point.width, 0)
}

let up = if is-horizontal {
(0, style.break-point.length)
} else {
(style.break-point.length, 0)
}

let add-break(is-end) = {
let a = ()
let b = (rel: vector.scale(size, .3), update: false)
let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false)
let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false)
let e = (rel: vector.scale(size, .7), update: false)
let f = (rel: size)

let mark = if is-end {
style.at("mark", default: none)
}
draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark)
}

draw.merge-path({
draw.move-to(start)
if axis.min > 0 {
add-break(false)
draw.line((rel: size, to: start), end, mark: style.at("mark", default: none))
} else if axis.max < 0 {
draw.line(start, (rel: vector.scale(size, -1), to: end))
add-break(true)
}
}, stroke: style.stroke)
} else {
draw.line(start, end, stroke: style.stroke, mark: style.at("mark", default: none))
}
}
139 changes: 139 additions & 0 deletions src/axes/formats.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// 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
///
/// ```example
/// plot.plot(size: (5,1),
/// x-format: axes.formats.fraction,
/// x-tick-step: 1/5,
/// y-tick-step: none, {
/// plot.add(calc.sin, domain: (-1, 1))
/// })
/// ```
///
/// - 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(size: (5,1),
/// x-format: axes.formats.multiple-of,
/// x-tick-step: calc.pi/4,
/// y-tick-step: none, {
/// 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
///
/// ```example
/// plot.plot(size: (5,1),
/// x-format: axes.formats.sci,
/// x-tick-step: 1e3,
/// y-tick-step: none, {
/// plot.add(x => x, domain: (-2e3, 2e3))
/// })
/// ```
///
/// - 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$
}
45 changes: 45 additions & 0 deletions src/axes/grid.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#import "/src/cetz.typ": util, vector, draw

#let _get-grid-type(axis) = {
let grid = axis.ticks.at("grid", default: false)
if grid == "major" or grid == true { return 1 }
if grid == "minor" { return 2 }
if grid == "both" { return 3 }
return 0
}

// Draw grid lines for the ticks of an axis
//
// - cxt (context):
// - axis (dictionary): The axis
// - ticks (array): The computed ticks
// - low (vector): Start position of a grid-line at tick 0
// - high (vector): End position of a grid-line at tick 0
// - 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) = {
let offset = (0,0)
if axis.inset != none {
let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v))
offset = vector.scale(vector.norm(dir), inset-low)
dir = vector.sub(dir, vector.scale(vector.norm(dir), inset-low + inset-high))
}

let kind = _get-grid-type(axis)
if kind > 0 {
for (distance, label, is-major) in ticks {
let offset = vector.add(vector.scale(dir, distance), offset)
let start = vector.add(low, offset)
let end = vector.add(high, offset)

// Draw a major line
if is-major and (kind == 1 or kind == 3) {
draw.line(start, end, stroke: style.grid.stroke)
}
// Draw a minor line
if not is-major and kind >= 2 {
draw.line(start, end, stroke: style.minor-grid.stroke)
}
}
}
}
3 changes: 3 additions & 0 deletions src/axes/preset.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#import "presets/scientific.typ"
#import "presets/school-book.typ": school-book
#import "presets/scientific-polar.typ": scientific-polar
139 changes: 139 additions & 0 deletions src/axes/presets/school-book.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#import "/src/cetz.typ": util, draw, styles, vector
#import "../style.typ": default-style, _prepare-style, _get-axis-style
#import "../draw.typ": _inset-axis-points, _draw-axis-line
#import "../grid.typ": draw-grid-lines
#import "../ticks.typ": *

#let default-style-schoolbook = util.merge-dictionary(default-style, (
x: (stroke: auto, fill: none, mark: (start: none, end: "straight"),
tick: (label: (anchor: "north"))),
y: (stroke: auto, fill: none, mark: (start: none, end: "straight"),
tick: (label: (anchor: "east"))),
label: (offset: .1cm),
origin: (label: (offset: .05cm)),
padding: .1cm, // Axis padding on both sides outsides the plotting area
overshoot: .5cm, // Axis end "overshoot" out of the plotting area
tick: (
offset: -50%,
minor-offset: -50%,
length: .2cm,
minor-length: 70%,
),
shared-zero: $0$, // Show zero tick label at (0, 0)
))

// Draw two axes in a "school book" style
//
// - x-axis (axis): X axis
// - y-axis (axis): Y axis
// - size (array): Size (width, height)
// - x-position (number): X Axis position
// - y-position (number): Y Axis position
// - name (string): Object name
// - ..style (any): Style
#let school-book(x-axis, y-axis,
size: (1, 1),
x-position: 0,
y-position: 0,
name: none,
..style) = {
import draw: *

group(name: name, ctx => {
let (w, h) = size
anchor("origin", (0, 0))

let style = style.named()
style = styles.resolve(
ctx.style,
merge: style,
root: "axes",
base: default-style-schoolbook)
style = _prepare-style(ctx, style)

let x-position = calc.min(calc.max(y-axis.min, x-position), y-axis.max)
let y-position = calc.min(calc.max(x-axis.min, y-position), x-axis.max)
let x-y = value-on-axis(y-axis, x-position) * h
let y-x = value-on-axis(x-axis, y-position) * w

let shared-zero = style.shared-zero != false and x-position == 0 and y-position == 0

let x-ticks = compute-ticks(x-axis, style, add-zero: not shared-zero)
let y-ticks = compute-ticks(y-axis, style, add-zero: not shared-zero)

// Draw grid
group(name: "grid", ctx => {
let axes = (
("x", (0,0), (0,h), (+w,0), x-ticks, x-axis),
("y", (0,0), (w,0), (0,+h), y-ticks, y-axis),
)

for (name, start, end, direction, ticks, axis) in axes {
if axis == none { continue }

let style = _get-axis-style(ctx, style, name)
on-layer(style.grid-layer, {
draw-grid-lines(ctx, axis, ticks, start, end, direction, style)
})
}
})

// Draw axes
group(name: "axes", {
let axes = (
("x", (0, x-y), (w, x-y), (1, 0), false, x-ticks, x-axis),
("y", (y-x, 0), (y-x, h), (0, 1), true, y-ticks, y-axis),
)
let label-pos = (
x: ("north", (0,-1)),
y: ("east", (-1,0)),
)

on-layer(style.axis-layer, {
for (name, start, end, dir, flip, ticks, axis) in axes {
let style = _get-axis-style(ctx, style, name)

let pad = style.padding
let overshoot = style.overshoot
let vstart = vector.sub(start, vector.scale(dir, pad))
let vend = vector.add(end, vector.scale(dir, pad + overshoot))
let is-horizontal = name == "x"

let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end)
group(name: "axis", {
_draw-axis-line(vstart, vend, axis, is-horizontal, style)
place-ticks-on-line(ticks, data-start, data-end, style, flip: flip)
})

if axis.label != none {
let (content-anchor, offset-dir) = label-pos.at(name)

let angle = if style.label.angle not in (none, auto) {
style.label.angle
} else { 0deg }
if style.label.anchor not in (none, auto) {
content-anchor = style.label.anchor
}

let offset = vector.scale(offset-dir, style.label.offset)
content((rel: offset, to: vend),
[#axis.label],
angle: angle,
anchor: content-anchor)
}
}

if shared-zero {
let pt = (rel: (-style.tick.label.offset, -style.tick.label.offset),
to: (y-x, x-y))
let zero = if type(style.shared-zero) == content {
style.shared-zero
} else {
$0$
}
content(pt, zero, anchor: "north-east")
}
})
})
})
}
Loading
Loading