|
863 | 863 | // Normalise a single `guide-axis*` spec to the flat shape consumers expect. |
864 | 864 | // Always carries a `stack` flag so callers can branch on the same field |
865 | 865 | // regardless of whether the guide originated from a stack or stand-alone. |
866 | | -#let _normalise-axis-guide(g) = ( |
867 | | - angle: g.at("angle", default: 0), |
868 | | - n-dodge: calc.max(1, g.at("n-dodge", default: 1)), |
869 | | - logticks: g.at("logticks", default: false), |
870 | | - stack: false, |
871 | | -) |
| 866 | +// `default-angle` (degrees, from the axis-text theme element) applies when the |
| 867 | +// guide leaves `angle` at its `0` default, so `guide-axis(angle:)` overrides |
| 868 | +// the theme but the theme still drives rotation on its own. |
| 869 | +#let _normalise-axis-guide(g, default-angle) = { |
| 870 | + let angle = g.at("angle", default: 0) |
| 871 | + ( |
| 872 | + angle: if angle != 0 { angle } else { default-angle }, |
| 873 | + n-dodge: calc.max(1, g.at("n-dodge", default: 1)), |
| 874 | + logticks: g.at("logticks", default: false), |
| 875 | + stack: false, |
| 876 | + ) |
| 877 | +} |
872 | 878 |
|
873 | 879 | // Read a `guide-axis(...)` or `guide-axis-stack(...)` configuration off the |
874 | 880 | // plot spec. Single guides flatten to `(angle, n-dodge, logticks, stack)`; |
875 | 881 | // stacks add `(guides, spacing)` plus aggregate fields so flat (non-stack) |
876 | 882 | // callers (label-anchor, log-minors) still see a sensible single-row view. |
877 | | -#let _read-axis-guide(spec, aes) = { |
| 883 | +#let _read-axis-guide(spec, aes, default-angle: 0) = { |
878 | 884 | let g = spec.at("guides", default: (:)).at(aes, default: none) |
879 | | - if g == none { return (angle: 0, n-dodge: 1, logticks: false, stack: false) } |
880 | | - if not g.at("stack", default: false) { return _normalise-axis-guide(g) } |
881 | | - let subs = g.guides.map(_normalise-axis-guide) |
| 885 | + if g == none { |
| 886 | + return (angle: default-angle, n-dodge: 1, logticks: false, stack: false) |
| 887 | + } |
| 888 | + if not g.at("stack", default: false) { |
| 889 | + return _normalise-axis-guide(g, default-angle) |
| 890 | + } |
| 891 | + let subs = g.guides.map(s => _normalise-axis-guide(s, default-angle)) |
882 | 892 | if subs.len() == 0 { |
883 | 893 | panic( |
884 | 894 | "guide-axis-stack requires at least one sub-guide; got an empty list.", |
|
894 | 904 | ) |
895 | 905 | } |
896 | 906 |
|
| 907 | +// Tick-label rotation in degrees read off the `axis-text` theme element for |
| 908 | +// the given aesthetic, used as the `guide-axis(angle:)` default so a theme can |
| 909 | +// rotate tick labels on its own. |
| 910 | +#let _axis-text-angle(theme, aes) = { |
| 911 | + let a = _text-style(theme, "axis-text-" + aes).angle |
| 912 | + if a == none { 0 } else { a.deg() } |
| 913 | +} |
| 914 | + |
897 | 915 | // Cap trim is a small wedge at the named axis-arc end, capped at 2° so it |
898 | 916 | // stays a visible-but-modest "fade" no matter how wide the theta sweep is. |
899 | 917 | #let _THETA-CAP-FRAC = 0.02 |
|
1234 | 1252 | let _len-side = (p, s, a) => _scalar-cascade(theme, p, s, a) / 1cm |
1235 | 1253 | let _tick-len = _per-side(_len-side, "tick-length") |
1236 | 1254 |
|
1237 | | - let x-guide = _read-axis-guide(spec, "x") |
1238 | | - let y-guide = _read-axis-guide(spec, "y") |
| 1255 | + let x-guide = _read-axis-guide(spec, "x", default-angle: _axis-text-angle( |
| 1256 | + theme, |
| 1257 | + "x", |
| 1258 | + )) |
| 1259 | + let y-guide = _read-axis-guide(spec, "y", default-angle: _axis-text-angle( |
| 1260 | + theme, |
| 1261 | + "y", |
| 1262 | + )) |
1239 | 1263 | let _x-label-anchor(angle) = { |
1240 | 1264 | if angle == 0 { "north" } else if angle > 0 { "north-east" } else { |
1241 | 1265 | "north-west" |
|
3086 | 3110 | let a = if style.align != none { style.align } else { default-align } |
3087 | 3111 | let args = _text-args(style) |
3088 | 3112 | for (k, v) in text-args.named() { args.insert(k, v) } |
3089 | | - box( |
3090 | | - width: inner-w, |
3091 | | - align( |
3092 | | - a, |
3093 | | - text(..args)[#resolve-prose(value, eval-strings: style.typst)], |
3094 | | - ), |
3095 | | - ) |
| 3113 | + let body = text(..args)[#resolve-prose(value, eval-strings: style.typst)] |
| 3114 | + if style.angle != none { body = rotate(style.angle, reflow: true, body) } |
| 3115 | + box(width: inner-w, align(a, body)) |
3096 | 3116 | } else { none } |
3097 | 3117 | let tag-block = if labs != none { |
3098 | 3118 | _chrome-block(labs.tag, tag, left, weight: tag.weight) |
|
3453 | 3473 | "y", |
3454 | 3474 | ) |
3455 | 3475 |
|
3456 | | - let x-guide = _read-axis-guide(spec, "x") |
3457 | | - let y-guide = _read-axis-guide(spec, "y") |
| 3476 | + let x-guide = _read-axis-guide( |
| 3477 | + spec, |
| 3478 | + "x", |
| 3479 | + default-angle: _axis-text-angle(theme, "x"), |
| 3480 | + ) |
| 3481 | + let y-guide = _read-axis-guide( |
| 3482 | + spec, |
| 3483 | + "y", |
| 3484 | + default-angle: _axis-text-angle(theme, "y"), |
| 3485 | + ) |
3458 | 3486 | // Themes that disable tick labels (`theme-void`) reserve no perpendicular |
3459 | 3487 | // depth for them; otherwise the chrome margin reserves space for ink that |
3460 | 3488 | // never draws, inverting the panel rect on small plot sizes. |
|
0 commit comments