From 5c99ee6a375e056f395790ce354069d5691e0058 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sat, 16 Nov 2024 14:07:35 +0100 Subject: [PATCH 01/21] WIP --- src/axis.typ | 35 ++++++++ src/projection.typ | 34 ++++++++ src/ticks.typ | 208 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 src/axis.typ create mode 100644 src/projection.typ create mode 100644 src/ticks.typ diff --git a/src/axis.typ b/src/axis.typ new file mode 100644 index 0000000..a5b697f --- /dev/null +++ b/src/axis.typ @@ -0,0 +1,35 @@ + +/// Transform linear axis value to linear space (low, high) +#let _transform-lin(ax, value, low, high) = { + let range = high - low + + return (value - ax.low) * (range / (ax.high - ax.low)) +} + +/// Transform log axis value to linear space (low, high) +#let _transform-log(ax, value, low, high) = { + let range = high - low + + let f(x) = { + calc.log(calc.max(x, util.float-epsilon), base: ax.base) + } + + return (value - f(ax.low)) * (range / (f(ax.high) - f(ax.low))) +} + +#let linear(low, high) = ( + low: low, high: high, transform: _transform-lin, +) + +#let logarithmic(low, high, base) = ( + low: low, high: high, base: base, transform: _transform-log, +) + +/// Transform an axis value to a linear value between low and high +/// - ax (axis): Axis +/// - value (number): Value to transform from axis space to linear space +/// - low (number): Linear minimum +/// - high (number): Linear maximum +#let transform(ax, value, low, high) = { + return (ax.transform)(ax, value, low, high) +} diff --git a/src/projection.typ b/src/projection.typ new file mode 100644 index 0000000..66445d2 --- /dev/null +++ b/src/projection.typ @@ -0,0 +1,34 @@ + +/// Create a new cartesian projection between two vectors, low and high +/// +/// - low (vector): Low vector +/// - high (vector): High vector +/// - x (axis): X axis +/// - y (axis): Y axis +/// - z (axis): Z axis +/// -> function Transformation for one or more vectors +#let cartesian(low, high, x, y, z) = { + let axes = (x, y, z) + + return (..v) = { + return v.pos().map(v => { + for i range(0, v.len()) { + v.at(i) = (axes.at(i).transform)(axes.at(i), v.at(i), low.at(i), high.at(i)) + } + }) + } +} + +/// - center (vector): Center vector +/// - start (angle): Start angle (0deg for full circle) +/// - stop (angle): Stop angle (360deg for full circle) +/// - theta (axis): Theta axis +/// - r (axis): R axis +/// -> function Transformation for one or more vectors +#let polar(center, radius, start, stop, theta, r) = { + return (..v) => { + let v = v.pos() + // TODO + return v + } +} diff --git a/src/ticks.typ b/src/ticks.typ new file mode 100644 index 0000000..d53156e --- /dev/null +++ b/src/ticks.typ @@ -0,0 +1,208 @@ +// Compute list of linear ticks for axis +// +// - axis (axis): Axis +#let compute-linear-ticks(axis, style, add-zero: true) = { + let (min, max) = (axis.min, axis.max) + let dt = max - min; if (dt == 0) { dt = 1 } + let ticks = axis.ticks + let ferr = util.float-epsilon + 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(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.minor-step + + let num-ticks = int(max * s + 1.5) - int(min * s) + assert(num-ticks <= minor-tick-limit, + message: "Number of minor ticks exceeds limit " + str(minor-tick-limit)) + + let n = range(int(min * s), int(max * s + 1.5)) + for t in n { + let v = (t / s - min) / dt + if v in major-tick-values { + // Prefer major ticks over minor ticks + continue + } + + if v != none and v >= 0 and v <= 1 + ferr { + l.push((v, none, false)) + } + } + } + + } + + 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 +#let fixed-ticks(axis) = { + let l = () + if "list" in axis.ticks { + for t in axis.ticks.list { + let (v, label) = (none, none) + if type(t) in (float, int) { + v = t + label = format-tick-value(t, axis.ticks) + } else { + (v, label) = t + } + + v = value-on-axis(axis, v) + if v != none and v >= 0 and v <= 1 { + l.push((v, label, true)) + } + } + } + return l +} + +// Compute list of axis ticks +// +// A tick triple has the format: +// (rel-value: float, label: content, major: bool) +// +// - axis (axis): Axis object +#let compute-ticks(axis, style, add-zero: true) = { + let find-max-n-ticks(axis, n: 11) = { + let dt = calc.abs(axis.max - axis.min) + let scale = calc.floor(calc.log(dt, base: 10) - 1) + if scale > 5 or scale < -5 {return none} + + let (step, best) = (none, 0) + for s in style.auto-tick-factors { + s = s * calc.pow(10, scale) + + let divs = calc.abs(dt / s) + if divs >= best and divs <= n { + step = s + best = divs + } + } + return step + } + + if axis == none or axis.ticks == none { return () } + if axis.ticks.step == auto { + axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count) + } + if axis.ticks.minor-step == auto { + axis.ticks.minor-step = if axis.ticks.step != none { + axis.ticks.step / 5 + } else { + none + } + } + + 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 +} From 3e3af570c2680754a6968c30f224fa4d948341d9 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sat, 16 Nov 2024 16:32:36 +0100 Subject: [PATCH 02/21] Some ideas, some tests --- src/axes.typ | 30 --- src/axis.typ | 34 +++- src/lib.typ | 1 - src/plot.typ | 438 +++++++++++++++---------------------------- src/plot/legend.typ | 5 + src/plot/line.typ | 248 +++++++++++------------- src/plot/mark.typ | 1 - src/plot/util.typ | 157 ++++++++-------- src/projection.typ | 43 +++-- src/spine.typ | 290 ++++++++++++++++++++++++++++ src/sub-plot.typ | 45 +++++ src/ticks.typ | 190 ++++++++++++++++--- tests/axes/ref/1.png | Bin 29345 -> 0 bytes tests/axes/test.typ | 65 ------- 14 files changed, 892 insertions(+), 655 deletions(-) create mode 100644 src/spine.typ create mode 100644 src/sub-plot.typ delete mode 100644 tests/axes/ref/1.png delete mode 100644 tests/axes/test.typ diff --git a/src/axes.typ b/src/axes.typ index 19cefdb..dd01c8e 100644 --- a/src/axes.typ +++ b/src/axes.typ @@ -1,36 +1,6 @@ #import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process #import "/src/plot/formats.typ" -/// Default axis style -/// -/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.]) -/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.]) -/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.]) -/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.]) -/// #show-parameter-block("stroke", "stroke", [Axis stroke style.]) -/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.]) -/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.]) -/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.]) -/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see @@on-layer() )]) -/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see @@on-layer() )]) -/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see @@on-layer() )]) -/// #show-parameter-block("padding", "number", [Extra distance between axes and plotting area. For schoolbook axes, this is the length of how much axes grow out of the plotting area.]) -/// #show-parameter-block("overshoot", "number", [School-book style axes only: Extra length to add to the end (right, top) of axes.]) -/// #show-parameter-block("tick.stroke", "stroke", [Major tick stroke style.]) -/// #show-parameter-block("tick.minor-stroke", "stroke", [Minor tick stroke style.]) -/// #show-parameter-block("tick.offset", ("number", "ratio"), [Major tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.minor-offset", ("number", "ratio"), [Minor tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.length", ("number"), [Major tick length.]) -/// #show-parameter-block("tick.minor-length", ("number", "ratio"), [Minor tick length, can be relative to the major tick length.]) -/// #show-parameter-block("tick.label.offset", ("number"), [Major tick label offset away from the tick.]) -/// #show-parameter-block("tick.label.angle", ("angle"), [Major tick label angle.]) -/// #show-parameter-block("tick.label.anchor", ("anchor"), [Anchor of major tick labels used for positioning.]) -/// #show-parameter-block("tick.label.show", ("auto", "bool"), default: auto, [Set visibility of tick labels. A value of `auto` shows tick labels for all but mirrored axes.]) -/// #show-parameter-block("grid.stroke", "stroke", [Major grid line stroke style.]) -/// #show-parameter-block("break-point.width", "number", [Axis break width along the axis.]) -/// #show-parameter-block("break-point.length", "number", [Axis break length.]) -/// #show-parameter-block("minor-grid.stroke", "stroke", [Minor grid line stroke style.]) -/// #show-parameter-block("shared-zero", ("bool", "content"), default: "$0$", [School-book style axes only: Content to display at the plots origin (0,0). If set to `false`, nothing is shown. Having this set, suppresses auto-generated ticks for $0$!]) #let default-style = ( tick-limit: 100, minor-tick-limit: 1000, diff --git a/src/axis.typ b/src/axis.typ index a5b697f..3d22444 100644 --- a/src/axis.typ +++ b/src/axis.typ @@ -1,9 +1,11 @@ +#import "/src/ticks.typ" +#import "/src/plot/util.typ" /// Transform linear axis value to linear space (low, high) #let _transform-lin(ax, value, low, high) = { let range = high - low - return (value - ax.low) * (range / (ax.high - ax.low)) + return low + (value - ax.min) * (range / (ax.max - ax.min)) } /// Transform log axis value to linear space (low, high) @@ -14,17 +16,37 @@ calc.log(calc.max(x, util.float-epsilon), base: ax.base) } - return (value - f(ax.low)) * (range / (f(ax.high) - f(ax.low))) + return low + (f(value) - f(ax.min)) * (range / (f(ax.max) - f(ax.min))) } -#let linear(low, high) = ( - low: low, high: high, transform: _transform-lin, +/// Linear Axis Constructor +#let linear(name, min, max) = ( + label: [#name], + name: name, min: min, max: max, base: 10, transform: _transform-lin, + auto-domain: (none, none), + ticks: (step: auto, minor-step: none, format: auto, list: none), + grid: none, + compute-ticks: ticks.compute-ticks.with("lin"), ) -#let logarithmic(low, high, base) = ( - low: low, high: high, base: base, transform: _transform-log, +/// Log Axis Constructor +#let logarithmic(name, min, max, base) = ( + label: [#name], + name: name, min: min, max: max, base: base, transform: _transform-log, + auto-domain: (none, none), + ticks: (step: auto, minor-step: none, format: auto, list: none), + grid: none, + compute-ticks: ticks.compute-ticks.with("log"), ) +// Prepare axis +#let prepare(ptx, ax) = { + if ax.min == none { ax.min = ax.auto-domain.at(0) } + if ax.max == none { ax.max = ax.auto-domain.at(1) } + if "compute-ticks" in ax { ax.computed-ticks = (ax.compute-ticks)(ax) } + return ax +} + /// Transform an axis value to a linear value between low and high /// - ax (axis): Axis /// - value (number): Value to transform from axis space to linear space diff --git a/src/lib.typ b/src/lib.typ index 128ff08..78a0fbf 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,5 +1,4 @@ #let version = version(0,1,0) -#import "/src/axes.typ" #import "/src/plot.typ" #import "/src/chart.typ" diff --git a/src/plot.typ b/src/plot.typ index 78451f4..5e3da4f 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -1,7 +1,13 @@ -#import "/src/cetz.typ": util, draw, matrix, vector, styles, palette +#import "/src/cetz.typ" +#import cetz: util, draw, matrix, vector, styles, palette, coordinate, styles #import util: bezier -#import "/src/axes.typ" +#import "/src/axis.typ" +#import "/src/projection.typ" +#import "/src/spine.typ" +#import "/src/ticks.typ" +#import "/src/sub-plot.typ" + #import "/src/plot/sample.typ": sample-fn, sample-fn2 #import "/src/plot/line.typ": add, add-hline, add-vline, add-fill-between #import "/src/plot/contour.typ": add-contour @@ -29,23 +35,43 @@ return default-plot-style(i) } -/// Add a cartesian axis to a plot -#let add-cartesian-axis(name, origin, target) = { - ((type: "context", fn: (ctx) => { - let axis = ( - name: name, - kind: "cartesian", - origin: origin, - target: target, - min: none, - max: none, - ticks: (step: auto, minor-step: none, format: "float", list: ()) - ) - ctx.axes.insert(name, axis) - return ctx +/// Add a linear axis to a plot +/// - name (str): Axis name +/// - min: (none, float): Minimum +/// - max: (none, float): Maximum +#let lin-axis(name, min: none, max: none) = { + ((priority: -100, fn: (ptx) => { + ptx.axes.insert(name, axis.linear(name, min, max)) + return ptx + }),) +} + +/// Add a logarithmic axis to a plot +/// - name (str): Axis name +/// - min: (none, float): Minimum +/// - max: (none, float): Maximum +/// - base: (int): Log base +#let log-axis(name, min: none, max: none, base: 10) = { + ((priority: -100, fn: (ptx) => { + ptx.axes.insert(name, axis.logarithmic(name, min, max, base)) + return ptx }),) } + +#let templates = ( + scientific: (ptx) => { + lin-axis("x") + lin-axis("y") + sub-plot.new("x", "y") + }, + school-book: (ptx) => { + lin-axis("x") + lin-axis("y") + sub-plot.new("x", "y") + }, +) + /// Create a plot environment. Data to be plotted is given by passing it to the /// `plot.add` or other plotting functions. The plot environment supports different /// axis styles to draw, see its parameter `axis-style:`. @@ -204,303 +230,141 @@ /// - legend-style (style): Style key-value overwrites for the legend style with style root `legend`. /// - ..options (any): Axis options, see _options_ below. #let plot(body, - size: (1, 1), - axis-style: "scientific", name: none, + size: (5, 4), + template: "scientific", plot-style: default-plot-style, - mark-style: default-mark-style, - fill-below: true, legend: auto, - legend-anchor: auto, + draw-legend: plot-legend.draw-legend, legend-style: (:), - ..options - ) = draw.group(name: name, cetz-ctx => { - draw.assert-version(version(0, 3, 1)) - - // Plot local context - let ctx = ( - origin: (0, 0), - size: size, - axes: (:) - ) - - // Setup default axes - let default-axes = ( - if axis-style in (none, "scientific", "scientific-auto") { - add-cartesian-axis("x", (0,0), (size.at(0),0)) - add-cartesian-axis("x2", (0,size.at(1)), (size.at(0),size.at(1))) - add-cartesian-axis("y", (0,0), (0,size.at(1))) - add-cartesian-axis("y2", (size.at(0),0), (size.at(0),size.at(1))) - } else if axis-style in ("school-book", "left") { - add-cartesian-axis("x", (0,0), (size.at(0),0)) - add-cartesian-axis("y", (0,0), (0,size.at(1))) - } + fill-below: true, + ..options) = draw.get-ctx(ctx => { + let body = body + let ptx = ( + cetz-ctx: ctx, + + default-size: size, + options: options.named(), + + axes: (:), // Shared axes + plots: (), // Sub plots + data: (), // Plot data + legend: (), // Legend entries + anchors: (), // Anchors ) - let body = default-axes + body - - // Create plot context object - let make-ctx(axes, size) = { - assert(axes.len() >= 1, message: "At least one axis must exist") - assert(size.at(0) > 0 and size.at(1) > 0, message: "Plot size must be > 0") - - return (axes: axes, size: size) + if template != none and template in templates { + body += (templates.at(template))(ptx) } - // Setup data viewport - let data-viewport(data, all-axes, body, name: none) = { - if body == none or body == () { return } + let plot-elements = body + .filter(elem => type(elem) == dictionary) + .sorted(key: elem => elem.at("priority", default: 0)) + let cetz-elements = body + .filter(elem => type(elem) == function) - // Setup the viewport - axes.axis-viewport(all-axes, body, name: name) - } + // Create axes + //ptx = plot-util.create-axes(ptx, plot-elements, options.named()) - let data = () - let anchors = () - let annotations = () - let body = if body != none { body } else { () } + for elem in plot-elements.filter(elem => elem.priority <= 0) { + assert("fn" in elem, + message: "Invalid plot element: " + repr(elem)) - for cmd in body { - assert(type(cmd) == dictionary and "type" in cmd, - message: "Expected plot sub-command in plot body") - if cmd.type == "anchor" { - anchors.push(cmd) - } else if cmd.type == "annotation" { - annotations.push(cmd) - } else if cmd.type == "context" { - ctx = (cmd.fn)(ctx) - } else { data.push(cmd) } + ptx = (elem.fn)(ptx) + assert(ptx != none) } - assert(axis-style in (none, "scientific", "scientific-auto", "school-book", "left"), - message: "Invalid plot style") - - - // Create axes for data & annotations - for d in data + annotations { - if "axes" not in d { continue } + // Apply axis options & prepare axes + ptx = plot-util.setup-axes(ptx, options.named()) - for (i, name) in d.axes.enumerate() { - assert(name in ctx.axes, message: "Undefined axis " + name) + for elem in plot-elements.filter(elem => elem.priority > 0) { + assert("fn" in elem, + message: "Invalid plot element: " + repr(elem)) - let axis = ctx.axes.at(name) - axis.used = true - - let domain = if i == 0 { - d.at("x-domain", default: (none, none)) - } else { - d.at("y-domain", default: (none, none)) - } - if domain != (none, none) { - axis.min = util.min(axis.min, ..domain) - axis.max = util.max(axis.max, ..domain) - } - - ctx.axes.at(name) = axis - } + ptx = (elem.fn)(ptx) + assert(ptx != none) } - // Set axis options - ctx.axes = plot-util.setup-axes(cetz-ctx, ctx.axes, options.named(), size) - // Prepare styles - for i in range(data.len()) { - if "style" not in data.at(i) { continue } - - let style-base = plot-style - if type(style-base) == function { - style-base = (style-base)(i) - } - assert.eq(type(style-base), dictionary, - message: "plot-style must be of type dictionary") - - if type(data.at(i).style) == function { - data.at(i).style = (data.at(i).style)(i) + ptx.data = ptx.data.enumerate().map(((i, data)) => { + let style = if type(plot-style) == function { + (plot-style)(i) + } else if type(plot-style) == array { + plot-style.at(i) + } else { + plot-style } - assert.eq(type(style-base), dictionary, - message: "data plot-style must be of type dictionary") - - data.at(i).style = util.merge-dictionary( - style-base, data.at(i).style) - if "mark-style" in data.at(i) { - let mark-style-base = mark-style - if type(mark-style-base) == function { - mark-style-base = (mark-style-base)(i) - } - assert.eq(type(mark-style-base), dictionary, - message: "mark-style must be of type dictionary") - - if type(data.at(i).mark-style) == function { - data.at(i).mark-style = (data.at(i).mark-style)(i) - } - - if type(data.at(i).mark-style) == dictionary { - data.at(i).mark-style = util.merge-dictionary( - mark-style-base, - data.at(i).mark-style - ) - } - } - } - - draw.group(name: "plot", { - draw.anchor("origin", (0, 0)) - - // Prepare - for i in range(data.len()) { - if "axes" not in data.at(i) { continue } - - let plot-ctx = make-ctx(data.at(i).axes.map(name => ctx.axes.at(name)), size) - - if "plot-prepare" in data.at(i) { - data.at(i) = (data.at(i).plot-prepare)(data.at(i), plot-ctx) - assert(data.at(i) != none, - message: "Plot prepare(self, cxt) returned none!") - } - } - - // Background Annotations - for a in annotations.filter(a => a.background) { - let plot-ctx = make-ctx(a.axes.map(name => ctx.axes.at(name)), size) - - data-viewport(a, plot-ctx.axes, { - draw.anchor("default", (0, 0)) - a.body - }) - } - - // Fill - if fill-below { - for d in data { - if "axes" not in d { continue } - - let plot-ctx = make-ctx(d.axes.map(name => ctx.axes.at(name)), size) - - data-viewport(d, plot-ctx.axes, { - draw.anchor("default", (0, 0)) - draw.set-style(..d.style) + data.style = cetz.util.merge-dictionary(style, data.at("style", default: (:))) + return data + }) - if "plot-fill" in d { - (d.plot-fill)(d, plot-ctx) + draw.group(name: name, { + draw.group(name: "plot", { + for sub-plot in ptx.plots { + let matching-data = () + for proj in sub-plot.projections { + let axis-names = proj.axes.map(ax => ax.name) + let sub-data = ptx.data.filter(data => data.axes.all(ax => ax in axis-names)) + if sub-data != () { + matching-data.push((proj, sub-data)) } - }) - } - } - - if axis-style in ("scientific", "scientific-auto") { - let draw-unset = if axis-style == "scientific" { - true - } else { - false - } - - let mirror = if axis-style == "scientific" { - auto - } else { - none - } - - axes.scientific( - size: size, - draw-unset: draw-unset, - bottom: ctx.axes.at("x", default: none), - top: ctx.axes.at("x2", default: mirror), - left: ctx.axes.at("y", default: none), - right: ctx.axes.at("y2", default: mirror),) - } else if axis-style == "left" { - axes.school-book( - size: size, - ctx.axes.x, - ctx.axes.y, - x-position: ctx.axes.y.min, - y-position: ctx.axes.x.min) - } else if axis-style == "school-book" { - axes.school-book( - size: size, - ctx.axes.x, - ctx.axes.y,) - } - - // Stroke + Mark data - for d in data { - if "axes" not in d { continue } - - let plot-ctx = make-ctx(d.axes.map(name => ctx.axes.at(name)), size) - - data-viewport(d, plot-ctx.axes, { - draw.anchor("default", (0, 0)) - draw.set-style(..d.style) + } - if not fill-below and "plot-fill" in d { - (d.plot-fill)(d, plot-ctx) + // Draw background + for (proj, sub-data) in matching-data { + for data in sub-data { + draw.scope({ + draw.set-style(..data.style) + if fill-below { + (data.fill)(ptx, proj.transform) + } + }) + } } - if "plot-stroke" in d { - (d.plot-stroke)(d, plot-ctx) + + // Draw spine (axes, ticks, ...) + if sub-plot.at("spine", default: none) != none { + draw.group(name: sub-plot.spine.name, { + (sub-plot.spine.draw)(ptx) + }) } - }) - if "mark" in d and d.mark != none { - draw.scope({ - draw.set-style(..d.style, ..d.mark-style) - mark.draw-mark(d.data, plot-ctx.axes, d.mark, d.mark-size) - }) + // Draw foreground + for (proj, sub-data) in matching-data { + for data in sub-data { + draw.scope({ + draw.set-style(..data.style) + if not fill-below { + (data.fill)(ptx, proj.transform) + } + (data.stroke)(ptx, proj.transform) + }) + } + } } - } - - // Foreground Annotations - for a in annotations.filter(a => not a.background) { - let plot-ctx = make-ctx(a.axes.map(name => ctx.axes.at(name)), size) - data-viewport(a, plot-ctx.axes, { - draw.anchor("default", (0, 0)) - a.body + draw.scope({ + cetz-elements }) - } - - // Place anchors - for a in anchors { - let plot-ctx = make-ctx(a.axes.map(name => ctx.axes.at(name)), size) - - let pt = a.position.enumerate().map(((i, v)) => { - if v == "min" { return plot-ctx.axes.at(i).min } - if v == "max" { return plot-ctx.axes.at(i).max } - return v + }) + + if ptx.legend != none { + draw.scope({ + draw.set-origin("plot." + options.at("legend", default: "north-east")) + draw.group(name: "legend", anchor: options.at("legend-anchor", default: "north-west"), { + draw.anchor("default", (0,0)) + draw-legend(ptx) + }) }) - pt = axes.transform-vec(plot-ctx.axes, pt) - if pt != none { - draw.anchor(a.name, pt) - } } - }) - // Draw the legend - if legend != none { - let items = data.filter(d => "label" in d and d.label != none) - if items.len() > 0 { - let legend-style = styles.resolve(cetz-ctx.style, - base: plot-legend.default-style, merge: legend-style, root: "legend") - - plot-legend.add-legend-anchors(legend-style, "plot", size) - plot-legend.legend(legend, anchor: legend-anchor, { - for item in items { - let preview = if "plot-legend-preview" in item { - _ => {(item.plot-legend-preview)(item) } - } else { - auto - } - - plot-legend.item(item.label, preview, - mark: item.at("mark", default: none), - mark-size: item.at("mark-size", default: none), - mark-style: item.at("mark-style", default: none), - ..item.at("style", default: (:))) - } - }, ..legend-style) + for (name, pt) in ptx.anchors { + draw.anchor(name, pt) } - } - draw.copy-anchors("plot") + draw.copy-anchors("plot") + }) }) /// Add an anchor to a plot environment @@ -529,9 +393,17 @@ /// as `add-anchors` does not create them on demand. #let add-anchor(name, position, axes: ("x", "y")) = { (( - type: "anchor", - name: name, - position: position, - axes: axes, + priority: 100, + fn: ptx => { + for plot in ptx.plots { + for proj in plot.projections { + if axes.all(name => proj.axes.contains(name)) { + let pt = (proj.transform)(position).first() + ptx.anchors.push((name, pt)) + } + } + } + return ptx + } ),) } diff --git a/src/plot/legend.typ b/src/plot/legend.typ index 475209b..859504b 100644 --- a/src/plot/legend.typ +++ b/src/plot/legend.typ @@ -240,3 +240,8 @@ (plot-legend-preview: _ => { preview() }) },) } + +// TODO: Stub +#let draw-legend(ptx) = { + draw.rect((0,0), (1,1)) +} diff --git a/src/plot/line.typ b/src/plot/line.typ index c709281..8fcc440 100644 --- a/src/plot/line.typ +++ b/src/plot/line.typ @@ -1,4 +1,4 @@ -#import "/src/cetz.typ": draw +#import "/src/cetz.typ": canvas, draw #import "util.typ" #import "sample.typ" @@ -7,7 +7,7 @@ // // - data (array): Data points // - line (str,dictionary): Line line -#let transform-lines(data, line) = { +#let _transform-lines(data, line) = { let hvh-data(t) = { if type(t) == ratio { t = t / 1% @@ -48,8 +48,8 @@ return util.linearized-data(data, line.at("epsilon", default: 0)) } else if line-type == "spline" { return util.sampled-spline-data(data, - line.at("tension", default: .5), - line.at("samples", default: 15)) + line.at("tension", default: .5), + line.at("samples", default: 15)) } else if line-type == "vh" { return hvh-data(0) } else if line-type == "hv" { @@ -62,7 +62,7 @@ } // Fill a plot by generating a fill path to y value `to` -#let fill-segments-to(segments, to) = { +#let _fill-segments-to(segments, to) = { for s in segments { let low = calc.min(..s.map(v => v.at(0))) let high = calc.max(..s.map(v => v.at(0))) @@ -75,55 +75,12 @@ } // Fill a shape by generating a fill path for each segment -#let fill-shape(paths) = { +#let _fill-shape(paths) = { for p in paths { draw.line(..p, stroke: none) } } -// Prepare line data -#let _prepare(self, ctx) = { - // Generate stroke paths - self.stroke-paths = util.compute-stroke-paths(self.line-data, ctx.axes) - - // 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, ctx.axes) - } - - return self -} - -// Stroke line data -#let _stroke(self, ctx) = { - for p in self.stroke-paths { - draw.line(..p, fill: none) - } -} - -// Fill line data -#let _fill(self, ctx) = { - let (x, y, ..) = ctx.axes - - if self.hypograph { - fill-segments-to(self.fill-paths, y.min) - } - if self.epigraph { - fill-segments-to(self.fill-paths, y.max) - } - if self.fill { - if self.at("fill-type", default: "axis") == "shape" { - fill-shape(self.fill-paths) - } else { - fill-segments-to(self.fill-paths, - calc.max(calc.min(y.max, 0), y.min)) - } - } -} - /// Add data to a plot environment. /// /// Note: You can use this for scatter plots by setting @@ -219,7 +176,7 @@ } // Transform data - let line-data = transform-lines(data, line) + let line-data = _transform-lines(data, line) // Get x-domain let x-domain = ( @@ -233,31 +190,54 @@ calc.max(..line-data.map(t => t.at(1))) )} - (( - type: "line", - label: label, - data: data, /* Raw data */ - line-data: line-data, /* Transformed data */ - axes: axes, - x-domain: x-domain, - y-domain: y-domain, - epigraph: epigraph, - hypograph: hypograph, - fill: fill, - fill-type: fill-type, - style: style, - mark: mark, - mark-size: mark-size, - mark-style: mark-style, - plot-prepare: _prepare, - plot-stroke: _stroke, - plot-fill: _fill, - plot-legend-preview: self => { - if self.fill or self.epigraph or self.hypograph { - draw.rect((0,0), (1,1), ..self.style) - } else { - draw.line((0,.5), (1,.5), ..self.style) - } + return (( + priority: 0, + fn: ptx => { + ptx = util.set-auto-domain(ptx, axes, (x-domain, y-domain)) + + ptx.data.push(( + label: label, + axes: axes, + fill: (ptx, proj) => { + let (x, y) = axes.map(name => ptx.axes.at(name)) + + if hypograph or epigraph or fill { + let (min-y, max-y) = proj((0, y.min), (0, y.max)).map(v => v.at(1)) + let fill-paths = util.compute-fill-paths(line-data, (x, y)) + .map(path => proj(..path)) + if hypograph { + _fill-segments-to(fill-paths, min-y) + } + if epigraph { + _fill-segments-to(fill-paths, max-y) + } + if fill { + if fill-type == "shape" { + _fill-shape(fill-paths) + } else { + _fill-segments-to(fill-paths, + calc.max(calc.min(max-y, 0), min-y)) + } + } + } + }, + stroke: (ptx, proj) => { + let (x, y) = axes.map(name => ptx.axes.at(name)) + + let stroke-paths = util.compute-stroke-paths(line-data, (x, y)) + .map(path => proj(..path)) + for path in stroke-paths { + draw.line(..path, fill: none) + } + }, + preview: () => { + // TODO + draw.rect((0,0), (2,1.5)) + }, + style: style, + )) + + return ptx } ),) } @@ -290,37 +270,33 @@ message: "Specify at least one y value") assert(y.named().len() == 0) - let prepare(self, ctx) = { - let (x, y, ..) = ctx.axes - let (x-min, x-max) = (x.min, x.max) - let (y-min, y-max) = (y.min, y.max) - let x-min = if min == auto { x-min } else { min } - let x-max = if max == auto { x-max } else { max } - - self.lines = self.y.filter(y => y >= y-min and y <= y-max) - .map(y => ((x-min, y), (x-max, y))) - return self - } - - let stroke(self, ctx) = { - for (a, b) in self.lines { - draw.line(a, b, fill: none) + return (( + priority: 0, + fn: ptx => { + let pts = y.pos() + + ptx.data.push(( + label: label, + axes: axes, + fill: (ptx, proj) => { + }, + stroke: (ptx, proj) => { + let (x, y) = axes.map(name => ptx.axes.at(name)) + + let min = if min == auto { x.min } else { min } + let max = if max == auto { x.max } else { max } + for pt in pts.filter(v => y.min <= v and v <= y.max) { + draw.line(..proj((min, pt), (max, pt))) + } + }, + preview: () => { + // TODO + }, + style: style, + )) + + return ptx } - } - - let x-min = if min == auto { none } else { min } - let x-max = if max == auto { none } else { max } - - (( - type: "hline", - label: label, - y: y.pos(), - x-domain: (x-min, x-max), - y-domain: (calc.min(..y.pos()), calc.max(..y.pos())), - axes: axes, - style: style, - plot-prepare: prepare, - plot-stroke: stroke, ),) } @@ -353,37 +329,33 @@ message: "Specify at least one x value") assert(x.named().len() == 0) - let prepare(self, ctx) = { - let (x, y, ..) = ctx.axes - let (x-min, x-max) = (x.min, x.max) - let (y-min, y-max) = (y.min, y.max) - let y-min = if min == auto { y-min } else { min } - let y-max = if max == auto { y-max } else { max } - - self.lines = self.x.filter(x => x >= x-min and x <= x-max) - .map(x => ((x, y-min), (x, y-max))) - return self - } - - let stroke(self, ctx) = { - for (a, b) in self.lines { - draw.line(a, b, fill: none) + return (( + priority: 0, + fn: ptx => { + let pts = x.pos() + + ptx.data.push(( + label: label, + axes: axes, + fill: (ptx, proj) => { + }, + stroke: (ptx, proj) => { + let (x, y) = axes.map(name => ptx.axes.at(name)) + + let min = if min == auto { y.min } else { min } + let max = if max == auto { y.max } else { max } + for pt in pts.filter(v => x.min <= v and v <= x.max) { + draw.line(..proj((pt, min), (pt, max))) + } + }, + preview: () => { + // TODO + }, + style: style, + )) + + return ptx } - } - - let y-min = if min == auto { none } else { min } - let y-max = if max == auto { none } else { max } - - (( - type: "vline", - label: label, - x: x.pos(), - x-domain: (calc.min(..x.pos()), calc.max(..x.pos())), - y-domain: (y-min, y-max), - axes: axes, - style: style, - plot-prepare: prepare, - plot-stroke: stroke ),) } @@ -433,8 +405,8 @@ } // Transform data - let line-a-data = transform-lines(data-a, line) - let line-b-data = transform-lines(data-b, line) + let line-a-data = _transform-lines(data-a, line) + let line-b-data = _transform-lines(data-b, line) // Get x-domain let x-domain = ( @@ -475,7 +447,7 @@ } let fill(self, ctx) = { - fill-shape(self.fill-paths) + _fill-shape(self.fill-paths) } (( diff --git a/src/plot/mark.typ b/src/plot/mark.typ index f085305..2582e79 100644 --- a/src/plot/mark.typ +++ b/src/plot/mark.typ @@ -1,5 +1,4 @@ #import "/src/cetz.typ": draw -#import "/src/axes.typ" #import "/src/plot/util.typ" // Draw mark at point with size diff --git a/src/plot/util.typ b/src/plot/util.typ index 70b5057..0bec863 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -2,6 +2,29 @@ #import cetz.util: bezier #import cetz: vector +#let float-epsilon = cetz.util.float-epsilon + +#let set-auto-domain(ptx, axes, dom) = { + let axes = axes.map(name => ptx.axes.at(name)) + assert(axes.len() == dom.len()) + + for (i, ax) in axes.enumerate() { + let (min, max) = ax.auto-domain + if min == none { + min = dom.at(i).at(0) + } else { + min = calc.min(min, dom.at(i).at(0)) + } + if max == none { + max = dom.at(i).at(1) + } else { + max = calc.max(max, dom.at(i).at(1)) + } + ptx.axes.at(ax.name).auto-domain = (min, max) + } + return ptx +} + /// Clip line-strip in rect /// /// - points (array): Array of vectors representing a line-strip @@ -275,105 +298,71 @@ return lower(name).starts-with("x") } +// Create axes specified by options +#let create-axes(ptx, elements, options) = { + import "/src/axis.typ" + for element in elements { + if "axes" in element { + for name in element.axes { + if not name in ptx.axes { + let mode = options.at(name + "-mode", default: "lin") + + ptx.axes.insert(name, if mode == "log" { + axis.logarithmic(name, none, none, 10) + } else { + axis.linear(name, none, none) + }) + } + } + } + } + + return ptx +} + // Setup axes dictionary // // - axis-dict (dictionary): Existing axis dictionary // - options (dictionary): Named arguments -// - plot-size (tuple): Plot width, height tuple -#let setup-axes(ctx, axis-dict, options, plot-size) = { - import "/src/axes.typ" +#let setup-axes(ptx, options) = { + import "/src/axis.typ" + let axes = ptx.axes // Get axis option for name - let get-axis-option(axis-name, name, default) = { + let get-opt(axis-name, name, default) = { let v = options.at(axis-name + "-" + name, default: default) if v == auto { default } else { v } } - for (name, axis) in axis-dict { - let used = axis.at("used", default: false) - - if not "ticks" in axis { axis.ticks = () } - axis.label = get-axis-option(name, "label", if used { $#name$ } else { axis.at("label", default: none) }) - - // Configure axis bounds - axis.min = get-axis-option(name, "min", axis.min) - axis.max = get-axis-option(name, "max", axis.max) - - if axis.min == none { - axis.min = 0 - axis.ticks.step = none - axis.ticks.minor-step = none - axis.ticks.format = none - } - if axis.max == none { axis.max = axis.min } - if axis.min == axis.max { - 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)) - - // Configure ticks - axis.ticks.list = get-axis-option(name, "ticks", ()) - axis.ticks.step = get-axis-option(name, "tick-step", axis.ticks.step) - axis.ticks.minor-step = get-axis-option(name, "minor-tick-step", axis.ticks.minor-step) - axis.ticks.decimals = get-axis-option(name, "decimals", 2) - axis.ticks.unit = get-axis-option(name, "unit", []) - axis.ticks.format = get-axis-option(name, "format", axis.ticks.format) - - // Axis break - axis.show-break = get-axis-option(name, "break", false) - axis.inset = get-axis-option(name, "inset", (0, 0)) - - // Configure grid - axis.ticks.grid = get-axis-option(name, "grid", false) - - axis-dict.at(name) = axis - } - - // Set axis options round two, after setting - // axis bounds - for (name, axis) in axis-dict { - let changed = false - - // Configure axis aspect ratio - let equal-to = get-axis-option(name, "equal", none) - if equal-to != none { - assert.eq(type(equal-to), str, - message: "Expected axis name.") - assert(equal-to != name, - message: "Axis can not be equal to itself.") - - let other = axis-dict.at(equal-to, default: none) - assert(other != none, - message: "Other axis must exist.") - assert(axis.kind == "cartesian" and other.kind == "cartesian", - message: "Bothe axes must be cartesian.") - - let dir = vector.sub(axis.target, axis.origin) - let other-dir = vector.sub(other.target, other.origin) - let ratio = vector.len(dir) / vector.len(other-dir) - - axis.min = other.min * ratio - axis.max = other.max * ratio - - changed = true - } - - if changed { - axis-dict.at(name) = axis + // Mode switching + for (name, ax) in axes { + let mode = get-opt(name, "mode", "lin") + if mode == "lin" { + ax.transform = axis._transform-lin + } else if mode == "log" { + ax.transform = axis._transform-log + ax.base = get-opt(name, "base", ax.at("base", default: 10)) + } else { + panic("Invalid axis mode: " + repr(mode)) } } - for (name, axis) in axis-dict { - axis-dict.at(name) = axes.prepare-axis(ctx, axis, name) + for (name, ax) in axes { + ax.min = get-opt(name, "min", ax.min) + ax.max = get-opt(name, "max", ax.max) + ax.label = get-opt(name, "label", ax.label) + ax.transform = get-opt(name, "transform", ax.transform) + ax.ticks.list = get-opt(name, "list", ax.ticks.list) + ax.ticks.format = get-opt(name, "format", ax.ticks.format) + ax.ticks.step = get-opt(name, "tick-step", ax.ticks.step) + ax.ticks.minor-step = get-opt(name, "minor-tick-step", ax.ticks.minor-step) + ax.grid = get-opt(name, "grid", ax.grid) + + axes.at(name) = axis.prepare(ptx, ax) } - return axis-dict + ptx.axes = axes + return ptx } /// Tests if point pt is contained in the diff --git a/src/projection.typ b/src/projection.typ index 66445d2..2c1fd5c 100644 --- a/src/projection.typ +++ b/src/projection.typ @@ -3,20 +3,24 @@ /// /// - low (vector): Low vector /// - high (vector): High vector -/// - x (axis): X axis -/// - y (axis): Y axis -/// - z (axis): Z axis +/// - axes (list): List of axes /// -> function Transformation for one or more vectors -#let cartesian(low, high, x, y, z) = { - let axes = (x, y, z) - - return (..v) = { - return v.pos().map(v => { - for i range(0, v.len()) { - v.at(i) = (axes.at(i).transform)(axes.at(i), v.at(i), low.at(i), high.at(i)) - } - }) - } +#let cartesian(low, high, axes) = { + return ( + axes: axes, + transform: (..v) => { + return v.pos().map(v => { + for i in range(0, v.len()) { + if axes.at(i) != none { + v.at(i) = (axes.at(i).transform)(axes.at(i), v.at(i), low.at(i), high.at(i)) + } else { + v.at(i) = 0 + } + } + return v + }) + }, + ) } /// - center (vector): Center vector @@ -26,9 +30,12 @@ /// - r (axis): R axis /// -> function Transformation for one or more vectors #let polar(center, radius, start, stop, theta, r) = { - return (..v) => { - let v = v.pos() - // TODO - return v - } + return ( + axes: axes, + transform: (..v) => { + let v = v.pos() + // TODO + return v + }, + ) } diff --git a/src/spine.typ b/src/spine.typ new file mode 100644 index 0000000..806db23 --- /dev/null +++ b/src/spine.typ @@ -0,0 +1,290 @@ +#import "/src/cetz.typ" +#import cetz: vector, draw + +#import "/src/ticks.typ" +#import "/src/projection.typ" + +/// Default axis style +/// +/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.]) +/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.]) +/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.]) +/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.]) +/// +/// #show-parameter-block("stroke", "stroke", [Axis stroke style.]) +/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.]) +/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.]) +/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.]) +/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see @@on-layer() )]) +/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see @@on-layer() )]) +/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see @@on-layer() )]) +/// #show-parameter-block("padding", "number", [Extra distance between axes and plotting area. For schoolbook axes, this is the length of how much axes grow out of the plotting area.]) +/// #show-parameter-block("tick.stroke", "stroke", [Major tick stroke style.]) +/// #show-parameter-block("tick.minor-stroke", "stroke", [Minor tick stroke style.]) +/// #show-parameter-block("tick.offset", ("number", "ratio"), [Major tick offset along the tick's direction, can be relative to the length.]) +/// #show-parameter-block("tick.minor-offset", ("number", "ratio"), [Minor tick offset along the tick's direction, can be relative to the length.]) +/// #show-parameter-block("tick.length", ("number"), [Major tick length.]) +/// #show-parameter-block("tick.minor-length", ("number", "ratio"), [Minor tick length, can be relative to the major tick length.]) +/// #show-parameter-block("tick.label.offset", ("number"), [Major tick label offset away from the tick.]) +/// #show-parameter-block("tick.label.angle", ("angle"), [Major tick label angle.]) +/// #show-parameter-block("tick.label.anchor", ("anchor"), [Anchor of major tick labels used for positioning.]) +/// #show-parameter-block("tick.label.show", ("auto", "bool"), default: auto, [Set visibility of tick labels. A value of `auto` shows tick labels for all but mirrored axes.]) +/// #show-parameter-block("grid.stroke", "stroke", [Major grid line stroke style.]) +/// #show-parameter-block("grid.minor-stroke", "stroke", [Minor grid line stroke style.]) +/// #show-parameter-block("break-point.width", "number", [Axis break width along the axis.]) +/// #show-parameter-block("break-point.length", "number", [Axis break length.]) +/// +/// #show-parameter-block("shared-zero", ("bool", "content"), default: "$0$", [School-book style axes only: Content to display at the plots origin (0,0). If set to `false`, nothing is shown. Having this set, suppresses auto-generated ticks for $0$!]) +#let default-style = ( + mark: none, + stroke: (paint: black, cap: "square"), + fill: none, + + padding: (0cm, 0cm), + + show-zero: true, + zero-label: $0$, + + axis-layer: 0, + tick-layer: 0, + grid-layer: 0, + + tick: ( + stroke: black + 1pt, + minor-stroke: black + .5pt, + + offset: 0cm, + length: .2cm, + minor-offset: 0cm, + minor-length: .1cm, + flip: false, + + label: ( + "show": auto, + offset: .1cm, + angle: 0deg, + anchor: "center", + draw: (pos, body, angle, anchor) => { + draw.content(pos, body, angle: angle, anchor: anchor) + }, + ), + ), + + grid: ( + stroke: black + .5pt, + minor-stroke: black + .25pt, + ), + + // Overrides + x: ( + tick: ( + label: ( + anchor: "north", + ), + ), + ), + y: ( + tick: ( + flip: true, + label: ( + anchor: "east", + ), + ), + ), + x2: ( + tick: ( + flip: true, + label: ( + anchor: "south", + ), + ), + ), + y2: ( + tick: ( + label: ( + anchor: "west", + ), + ), + ), +) + +/// Default schoolbook style +#let default-style-schoolbook = cetz.util.merge-dictionary(default-style, ( + mark: (end: "straight"), + padding: (.1cm, 1em), + + tick: ( + offset: -.1cm, + length: .2cm, + minor-offset: -.05cm, + minor-length: .1cm, + ), +)) + +#let _prepare-style(ptx, style) = { + let ctx = ptx.cetz-ctx + let resolve-number = cetz.util.resolve-number.with(ctx) + let relative-to(val, to) = { + return if type(val) == ratio { + val * to + } else { + val + } + } + let resolve-relative-number(val, to) = { + return resolve-number(relative-to(val, to)) + } + + if type(style.padding) != array { + style.padding = (style.padding,) * 2 + } + style.padding = style.padding.map(resolve-number) + + style.tick.offset = resolve-number(style.tick.offset) + style.tick.length = resolve-number(style.tick.length) + style.tick.minor-offset = resolve-relative-number(style.tick.minor-offset, style.tick.offset) + style.tick.minor-length = resolve-relative-number(style.tick.minor-length, style.tick.length) + + style.tick.label.offset = resolve-number(style.tick.label.offset) + + return style +} + +#let _get-axis-style(ptx, style, name) = { + return _prepare-style(ptx, if name in style { + cetz.util.merge-dictionary(style, style.at(name)) + } else { + style + }) +} + + +/// +#let cartesian-scientific(projections: none, style: (:)) = { + return ( + name: none, + draw: (ptx) => { + let proj = projections.at(0) + let axes = proj.axes + let x = axes.at(0) + let y = axes.at(1) + + let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + root: "axes", merge: style, base: default-style)) + let x-style = _get-axis-style(ptx, style, "x") + let y-style = _get-axis-style(ptx, style, "y") + let x2-style = _get-axis-style(ptx, style, "x2") + let y2-style = _get-axis-style(ptx, style, "y2") + + let (south-west, south-east, north-west, north-east) = (proj.transform)( + (x.min, y.min), (x.max, y.min), + (x.min, y.max), (x.max, y.max), + ) + + let x-padding = x-style.padding + let y-padding = y-style.padding + + let x-low = vector.add(south-west, (-x-padding.at(0), -y-padding.at(0))) + let x-high = vector.add(south-east, (+x-padding.at(1), -y-padding.at(0))) + let y-low = vector.add(south-west, (-x-padding.at(0), -y-padding.at(0))) + let y-high = vector.add(north-west, (-x-padding.at(0), y-padding.at(1))) + + let x2-low = vector.add(north-west, (-x-padding.at(0), y-padding.at(1))) + let x2-high = vector.add(north-east, (+x-padding.at(1), y-padding.at(1))) + let y2-low = vector.add(south-east, (x-padding.at(1), -y-padding.at(0))) + let y2-high = vector.add(north-east, (x-padding.at(1), y-padding.at(1))) + + let axes = ( + (x, 0, south-west, south-east, x-low, x-high, x-style, false), + (y, 1, south-west, north-west, y-low, y-high, y-style, false), + (x, 0, north-west, north-east, x2-low, x2-high, x2-style, true), + (y, 1, south-east, north-east, y2-low, y2-high, y2-style, true), + ) + + for (ax, component, low, high, frame-low, frame-high, style, mirror) in axes { + draw.on-layer(style.axis-layer, { + draw.line(frame-low, frame-high, stroke: style.stroke, mark: style.mark) + }) + if "computed-ticks" in ax { + let low = low.enumerate().map(((i, v)) => { + if i == component { v } else { frame-low.at(i) } + }) + let high = high.enumerate().map(((i, v)) => { + if i == component { v } else { frame-low.at(i) } + }) + + if not mirror { + ticks.draw-cartesian-grid(low, high, component, ax, ax.computed-ticks, (0,0), (1,0), style) + } + ticks.draw-cartesian(low, high, ax.computed-ticks, style, is-mirror: mirror) + } + } + }, + ) +} + +/// +#let schoolbook(projections: none, zero: (0, 0), ..style) = { + return ( + name: none, + draw: (ptx) => { + let proj = projections.at(0) + let axes = proj.axes + let x = axes.at(0) + let y = axes.at(1) + let z = axes.at(2, default: none) + + let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + root: "axes", merge: style.named(), base: default-style-schoolbook)) + let x-style = _get-axis-style(ptx, style, "x") + let y-style = _get-axis-style(ptx, style, "y") + + let (zero, min-x, max-x, min-y, max-y) = (proj.transform)( + zero, + vector.add(zero, (x.min, 0)), vector.add(zero, (x.max, 0)), + vector.add(zero, (0, y.min)), vector.add(zero, (0, y.max)), + ) + + let x-padding = x-style.padding + if type(x-padding) != array { + x-padding = (x-padding, x-padding) + } + + let y-padding = y-style.padding + if type(y-padding) != array { + y-padding = (y-padding, y-padding) + } + + let outset-lo-x = (x-padding.at(0), 0) + let outset-hi-x = (x-padding.at(1), 0) + let outset-lo-y = (0., y-padding.at(0)) + let outset-hi-y = (0., y-padding.at(1)) + let outset-min-x = vector.scale(outset-lo-x, -1) + let outset-max-x = vector.scale(outset-hi-x, +1) + let outset-min-y = vector.scale(outset-lo-y, -1) + let outset-max-y = vector.scale(outset-hi-y, +1) + + draw.on-layer(x-style.axis-layer, { + draw.line((rel: outset-min-x, to: min-x), + (rel: outset-max-x, to: max-x), + mark: x-style.mark, + stroke: x-style.stroke) + }) + if "computed-ticks" in x { + ticks.draw-cartesian-grid(min-x, max-x, 0, x, x.computed-ticks, min-y, max-y, x-style) + ticks.draw-cartesian(min-x, max-x, x.computed-ticks, x-style) + } + + draw.on-layer(y-style.axis-layer, { + draw.line((rel: outset-min-y, to: min-y), + (rel: outset-max-y, to: max-y), + mark: y-style.mark, + stroke: y-style.stroke) + }) + if "computed-ticks" in y { + ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) + ticks.draw-cartesian(min-y, max-y, y.computed-ticks, y-style) + } + } + ) +} diff --git a/src/sub-plot.typ b/src/sub-plot.typ new file mode 100644 index 0000000..4eebe85 --- /dev/null +++ b/src/sub-plot.typ @@ -0,0 +1,45 @@ +#import "/src/spine.typ" +#import "/src/projection.typ" +#import "/src/cetz.typ" + +/// +#let new(spine: spine.cartesian-scientific, projection: projection.cartesian, origin: (0, 0), size: auto, ..axes-style) = { + let axes = axes-style.pos() + let style = axes-style.named() + + assert(axes.len() > 0, + message: "Axes must be set!") + assert(calc.rem(axes.len(), 2) == 0, + message: "Axes must be a multiple of two!") + + (( + priority: 100, + fn: ptx => { + let (_, origin) = cetz.coordinate.resolve(ptx.cetz-ctx, origin) + let size = size + if size == auto { + size = ptx.default-size + } + size = size.map(cetz.util.resolve-number.with(ptx.cetz-ctx)).map(calc.abs) + size = cetz.vector.add(origin, size) + + let axes = axes.map(name => ptx.axes.at(name)) + let axis-pairs = axes.chunks(2, exact: true) + let projections = axis-pairs.map(((x, y)) => { + (projection)(origin, size, (x, y)) + }) + + let spine = if spine != none { + spine( + projections: projections, + ) + } + + ptx.plots.push(( + spine: spine, + projections: projections, + )) + return ptx + } + ),) +} diff --git a/src/ticks.typ b/src/ticks.typ index d53156e..8418b0d 100644 --- a/src/ticks.typ +++ b/src/ticks.typ @@ -1,13 +1,44 @@ +#import "/src/cetz.typ" +#import cetz: util, vector, draw + +#import "/src/plot/formats.typ" + +// Format a tick value +#let format-tick-value(value, tic-options) = { + // Without it we get negative zero in conversion + // to content! Typst has negative zero floats. + if value == 0 { value = 0 } + + if type(value) != std.content { + let format = tic-options.at("format", default: "float") + if format == none { + value = [] + } else if type(format) == std.content { + value = format + } else if type(format) == function { + value = (format)(value) + } else if format == "sci" { + value = formats.sci(value, digits: tic-options.at("decimals", default: 2)) + } else { + value = formats.decimal(value, digits: tic-options.at("decimals", default: 2)) + } + } else if type(value) != std.content { + value = str(value) + } + + return value +} + // Compute list of linear ticks for axis // // - axis (axis): Axis -#let compute-linear-ticks(axis, style, add-zero: true) = { +#let compute-linear-ticks(axis, add-zero: true) = { let (min, max) = (axis.min, axis.max) let dt = max - min; if (dt == 0) { dt = 1 } let ticks = axis.ticks let ferr = util.float-epsilon - let tick-limit = style.tick-limit - let minor-tick-limit = style.minor-tick-limit + let tick-limit = axis.at("tick-limit", default: 100) + let minor-tick-limit = axis.at("minor-tick-limit", default: 1000) let l = () if ticks != none { @@ -68,7 +99,7 @@ // Compute list of linear ticks for axis // // - axis (axis): Axis -#let compute-logarithmic-ticks(axis, style, add-zero: true) = { +#let compute-logarithmic-ticks(axis, add-zero: true) = { let ferr = util.float-epsilon let (min, max) = ( calc.log(calc.max(axis.min, ferr), base: axis.base), @@ -77,8 +108,8 @@ 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 tick-limit = axis.at("tick-limit", default: 100) + let minor-tick-limit = axis.at("minor-tick-limit", default: 1000) let l = () if ticks != none { @@ -140,25 +171,29 @@ // Get list of fixed axis ticks // // - axis (axis): Axis object -#let fixed-ticks(axis) = { - let l = () - if "list" in axis.ticks { - for t in axis.ticks.list { - let (v, label) = (none, none) - if type(t) in (float, int) { - v = t - label = format-tick-value(t, axis.ticks) - } else { - (v, label) = t - } +#let fixed-ticks(ax) = { + let list = ax.at("list", default: none) + if type(list) == function { + list = (list)(ax) + } + if type(list) != array { + return () + } - v = value-on-axis(axis, v) - if v != none and v >= 0 and v <= 1 { - l.push((v, label, true)) - } + return list.map(t => { + let (v, label) = (none, none) + if type(t) in (float, int) { + v = t + label = format-tick-value(t, axis.ticks) + } else { + (v, label) = t } - } - return l + + v = value-on-axis(axis, v) + if v != none and v >= 0 and v <= 1 { + l.push((v, label, true)) + } + }) } // Compute list of axis ticks @@ -166,15 +201,19 @@ // A tick triple has the format: // (rel-value: float, label: content, major: bool) // +// - mode (str): "lin" or "log" // - axis (axis): Axis object -#let compute-ticks(axis, style, add-zero: true) = { +#let compute-ticks(mode, axis, add-zero: true) = { + let auto-tick-count = 11 + let auto-tick-factors = (1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10) + let find-max-n-ticks(axis, n: 11) = { let dt = calc.abs(axis.max - axis.min) let scale = calc.floor(calc.log(dt, base: 10) - 1) if scale > 5 or scale < -5 {return none} let (step, best) = (none, 0) - for s in style.auto-tick-factors { + for s in auto-tick-factors { s = s * calc.pow(10, scale) let divs = calc.abs(dt / s) @@ -188,7 +227,7 @@ if axis == none or axis.ticks == none { return () } if axis.ticks.step == auto { - axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count) + axis.ticks.step = find-max-n-ticks(axis, n: auto-tick-count) } if axis.ticks.minor-step == auto { axis.ticks.minor-step = if axis.ticks.step != none { @@ -198,11 +237,104 @@ } } - let ticks = if axis.mode == "log" { - compute-logarithmic-ticks(axis, style, add-zero: add-zero) + let ticks = if mode == "log" { + compute-logarithmic-ticks(axis, add-zero: add-zero) } else { - compute-linear-ticks(axis, style, add-zero: add-zero) + compute-linear-ticks(axis, add-zero: add-zero) } ticks += fixed-ticks(axis) return ticks } + +// Place a list of tick marks and labels along a line +#let draw-cartesian(start, stop, ticks, style, is-mirror: false, show-zero: true) = { + let draw-label = style.tick.label.draw + + draw.on-layer(style.tick-layer, { + let dir = vector.norm(vector.sub(stop, start)) + let norm = (-dir.at(1), dir.at(0), dir.at(2, default: 0)) + + let def(v, d) = { + return if v == none or v == auto {d} else {v} + } + + let show-label = style.tick.label.show + if show-label == auto { + show-label = not is-mirror + } + + for (distance, label, is-major) in ticks { + let offset = if is-major { style.tick.offset } else { style.tick.minor-offset } + let length = if is-major { style.tick.length } else { style.tick.minor-length } + let stroke = if is-major { style.tick.stroke } else { style.tick.minor-stroke } + + let pt = vector.lerp(start, stop, distance) + if style.tick.flip { + offset = -offset - length + } + + let a = vector.add(pt, vector.scale(norm, offset)) + let b = vector.add(a, vector.scale(norm, length)) + + draw.line(a, b, stroke: stroke) + + if draw-label != none and show-label and label != none { + let offset = style.tick.label.offset + if style.tick.flip { + offset = -offset - length + } + + let angle = def(style.tick.label.angle, 0deg) + let anchor = def(style.tick.label.anchor, "center") + let pos = vector.sub(a, vector.scale(norm, offset)) + + draw-label(pos, label, angle, anchor) + } + } + }) +} + +// Draw grid lines for the ticks of an axis +// +// - ptx (context): Plot context +// - start (vector): Axis start +// - stop (vector): Axis stop +// - component (int): Vector compontent to use as direction +// - 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 +// - style (style): Style +#let draw-cartesian-grid(start, stop, component, axis, ticks, low, high, style) = { + let kind = if axis.grid in (true, "major") { + 1 + } else if axis.grid == "minor" { + 2 + } else if axis.grid == "both" { + 3 + } else { + 0 + } + + if kind > 0 { + draw.on-layer(style.grid-layer, { + for (distance, label, is-major) in ticks { + let offset = vector.lerp(start, stop, distance) + + let start = low + start.at(component) = offset.at(component) + let end = high + end.at(component) = offset.at(component) + + // Draw a minor line + if not is-major and kind >= 2 { + draw.line(start, end, stroke: style.grid.minor-stroke) + } + // Draw a major line + if is-major and (kind == 1 or kind == 3) { + draw.line(start, end, stroke: style.grid.stroke) + } + } + }) + } +} diff --git a/tests/axes/ref/1.png b/tests/axes/ref/1.png deleted file mode 100644 index fbc060371a2eca88a8f4b18df9c5a3d9d8d0b7d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29345 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 diff --git a/tests/axes/test.typ b/tests/axes/test.typ deleted file mode 100644 index a4b6153..0000000 --- a/tests/axes/test.typ +++ /dev/null @@ -1,65 +0,0 @@ -#set page(width: auto, height: auto) -#import "/tests/helper.typ": * -#import cetz: draw -#import cetz-plot: axes, plot - -// Schoolbook Axis Styling -#test-case({ - import draw: * - - set-style(axes: ( - stroke: blue, - padding: .25, - x: (stroke: red), - y: (stroke: green, tick: (stroke: blue, length: .3)) - )) - axes.school-book(size: (6, 6), - axes.axis(min: -1, max: 1, ticks: (step: 1, minor-step: auto, - grid: "both")), - axes.axis(min: -1, max: 1, ticks: (step: .5, minor-step: auto, - grid: "major"))) -}) - -// Scientific Axis Styling -#test-case({ - import draw: * - - set-style(axes: (stroke: blue)) - set-style(axes: (left: (tick: (stroke: green + 2pt)))) - set-style(axes: (bottom: (tick: (stroke: red, length: .5, - label: (angle: 90deg, - anchor: "east"))))) - set-style(axes: (right: (tick: (label: (offset: .2, - angle: -45deg, - anchor: "north-west"), length: -.1)))) - axes.scientific(size: (6, 6), - draw-unset: false, - top: none, - bottom: axes.axis(min: -1, max: 1, ticks: (step: 1, minor-step: auto, - grid: "both", format: plot.formats.decimal.with(prefix: $<-$, suffix: $->$))), - left: axes.axis(min: -1, max: 1, ticks: (step: .5, minor-step: auto, - grid: false)), - right: axes.axis(min: -10, max: 10, ticks: (step: auto, minor-step: auto, - grid: "major")),) -}) - -// Custom Tick Format -#test-case({ - import draw: * - - axes.scientific(size: (6, 1), - bottom: axes.axis(min: -2*calc.pi, max: 2*calc.pi, ticks: ( - step: calc.pi, minor-step: auto, format: plot.formats.multiple-of.with(symbol: $pi$), - )), - left: axes.axis(min: -1, max: 1, ticks: (step: none, minor-step: none))) -}) - -// #10 - Minor ticks on reversed axis -#test-case({ - import draw: * - - axes.scientific(size: (6, 1), - bottom: axes.axis(min: 5, max: -5, - ticks: (step: 5, minor-step: 1)), - left: axes.axis(min: -1, max: 1, ticks: (step: none, minor-step: none))) -}) From 3fb42ddfb49208a8384112ca1816bc71806d7070 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 25 Nov 2024 00:49:15 +0100 Subject: [PATCH 03/21] Add primitive polar plot support --- src/plot/util.typ | 4 ++++ src/projection.typ | 31 ++++++++++++++++---------- src/spine.typ | 54 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/plot/util.typ b/src/plot/util.typ index 0bec863..30830f2 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -20,6 +20,10 @@ } else { max = calc.max(max, dom.at(i).at(1)) } + if min == max { + min -= float-epsilon + max += float-epsilon + } ptx.axes.at(ax.name).auto-domain = (min, max) } return ptx diff --git a/src/projection.typ b/src/projection.typ index 2c1fd5c..caccbc1 100644 --- a/src/projection.typ +++ b/src/projection.typ @@ -1,8 +1,9 @@ +#import "/src/cetz.typ": vector -/// Create a new cartesian projection between two vectors, low and high +/// Create a new cartesian projection /// -/// - low (vector): Low vector -/// - high (vector): High vector +/// - low (vector): Lower viewport corner +/// - high (vector): Upper viewport corner /// - axes (list): List of axes /// -> function Transformation for one or more vectors #let cartesian(low, high, axes) = { @@ -23,19 +24,27 @@ ) } -/// - center (vector): Center vector +/// Create a new polar projection +/// +/// - low (vector): Lower viewport corner +/// - high (vector): Upper viewport corner /// - start (angle): Start angle (0deg for full circle) /// - stop (angle): Stop angle (360deg for full circle) -/// - theta (axis): Theta axis -/// - r (axis): R axis +/// - axes (list): Axis array (angular, distal) /// -> function Transformation for one or more vectors -#let polar(center, radius, start, stop, theta, r) = { +#let polar(low, high, (angular, distal, ..), start: 0deg, stop: 360deg) = { + let center = vector.lerp(low, high, .5) + let radius = calc.min(..vector.sub(high, low).slice(0, 2)) / 2 + return ( - axes: axes, + axes: (angular, distal), transform: (..v) => { - let v = v.pos() - // TODO - return v + return v.pos().map(v => { + let theta = (angular.transform)(angular, v.at(0), start, stop) + let r = (distal.transform)(distal, v.at(1), 0, radius) + + vector.add(center, (calc.cos(theta) * r, calc.sin(theta) * r, 0)) + }) }, ) } diff --git a/src/spine.typ b/src/spine.typ index 806db23..7bd4d99 100644 --- a/src/spine.typ +++ b/src/spine.typ @@ -106,6 +106,14 @@ ), ), ), + distal: ( + tick: ( + flip: true, + label: ( + anchor: "east", + ) + ) + ), ) /// Default schoolbook style @@ -160,9 +168,9 @@ /// -#let cartesian-scientific(projections: none, style: (:)) = { +#let cartesian-scientific(projections: none, name: none, style: (:)) = { return ( - name: none, + name: name, draw: (ptx) => { let proj = projections.at(0) let axes = proj.axes @@ -224,9 +232,9 @@ } /// -#let schoolbook(projections: none, zero: (0, 0), ..style) = { +#let schoolbook(projections: none, name: none, zero: (0, 0), ..style) = { return ( - name: none, + name: name, draw: (ptx) => { let proj = projections.at(0) let axes = proj.axes @@ -288,3 +296,41 @@ } ) } + +/// Polar frame +#let polar(projections: none, name: none, ..style) = { + assert(projections.len() == 1, + message: "Unexpected number of projections!") + + return ( + name: name, + draw: (ptx) => { + let proj = projections.first() + let angular = proj.axes.at(0) + let distal = proj.axes.at(1) + + let (origin, outer) = (proj.transform)((0, distal.min), (0, distal.max)) + let radius = vector.dist(origin, outer) + + let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + root: "axes", merge: style.named(), base: default-style)) + let angular-style = _get-axis-style(ptx, style, "angular") + let distal-style = _get-axis-style(ptx, style, "distal") + + let r-start = origin + let r-end = vector.add(origin, (0, radius)) + draw.line(r-start, r-end, stroke: distal-style.stroke) + if "computed-ticks" in distal { + //ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) + ticks.draw-cartesian(r-start, r-end, distal.computed-ticks, distal-style) + } + + let padding = angular-style.padding.first() + draw.circle(origin, radius: radius + padding, + stroke: angular-style.stroke) + if "computed-ticks" in angular { + // TODO + } + }, + ) +} From 755df34b707f0ee5774b33c58dc5aa344f06a23b Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 25 Nov 2024 01:11:48 +0100 Subject: [PATCH 04/21] polar: Suppor arc projections --- src/spine.typ | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/spine.typ b/src/spine.typ index 7bd4d99..be908f7 100644 --- a/src/spine.typ +++ b/src/spine.typ @@ -309,25 +309,47 @@ let angular = proj.axes.at(0) let distal = proj.axes.at(1) - let (origin, outer) = (proj.transform)((0, distal.min), (0, distal.max)) - let radius = vector.dist(origin, outer) + let (origin, start, mid, stop) = (proj.transform)( + (angular.min, distal.min), + (angular.min, distal.max), + ((angular.min + angular.max) / 2, distal.max), + (angular.max, distal.max), + ) + start = start.map(calc.round.with(digits: 6)) + stop = stop.map(calc.round.with(digits: 6)) + + let radius = vector.dist(origin, start) let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, root: "axes", merge: style.named(), base: default-style)) let angular-style = _get-axis-style(ptx, style, "angular") let distal-style = _get-axis-style(ptx, style, "distal") + let r-padding = angular-style.padding.first() let r-start = origin let r-end = vector.add(origin, (0, radius)) - draw.line(r-start, r-end, stroke: distal-style.stroke) + draw.line(r-start, (rel: (0, radius + r-padding)), stroke: distal-style.stroke) if "computed-ticks" in distal { + // TODO //ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) ticks.draw-cartesian(r-start, r-end, distal.computed-ticks, distal-style) } - let padding = angular-style.padding.first() - draw.circle(origin, radius: radius + padding, - stroke: angular-style.stroke) + if start == stop { + draw.circle(origin, radius: radius + r-padding, + stroke: angular-style.stroke, + fill: angular-style.fill) + } else { + // Apply padding to all three points + (start, mid, stop) = (start, mid, stop).map(pt => { + vector.add(pt, vector.scale(vector.norm(vector.sub(pt, origin)), r-padding)) + }) + + draw.arc-through(start, mid, stop, + stroke: angular-style.stroke, + fill: angular-style.fill, + mode: "PIE") + } if "computed-ticks" in angular { // TODO } From d43d7fb3249096cee73dcd22f0e8afbc686d02c8 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 25 Nov 2024 02:23:22 +0100 Subject: [PATCH 05/21] polar: Support polar grid --- src/spine.typ | 3 +- src/ticks.typ | 110 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/spine.typ b/src/spine.typ index be908f7..a88a92e 100644 --- a/src/spine.typ +++ b/src/spine.typ @@ -331,7 +331,7 @@ draw.line(r-start, (rel: (0, radius + r-padding)), stroke: distal-style.stroke) if "computed-ticks" in distal { // TODO - //ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) + ticks.draw-distal-grid(proj, distal.computed-ticks, distal-style) ticks.draw-cartesian(r-start, r-end, distal.computed-ticks, distal-style) } @@ -351,6 +351,7 @@ mode: "PIE") } if "computed-ticks" in angular { + ticks.draw-angular-grid(proj, angular.computed-ticks, angular-style) // TODO } }, diff --git a/src/ticks.typ b/src/ticks.typ index 8418b0d..8130192 100644 --- a/src/ticks.typ +++ b/src/ticks.typ @@ -3,6 +3,22 @@ #import "/src/plot/formats.typ" +#let _get-grid-mode(mode) = { + return if mode in (true, "major") { + 1 + } else if mode == "minor" { + 2 + } else if mode == "both" { + 3 + } else { + 0 + } +} + +#let _draw-grid(mode, is-major) = { + return mode >= 3 or (is-major and mode == 1) or mode == 2 +} + // Format a tick value #let format-tick-value(value, tic-options) = { // Without it we get negative zero in conversion @@ -306,16 +322,7 @@ // - high (vector): End position of a grid-line at tick 0 // - style (style): Style #let draw-cartesian-grid(start, stop, component, axis, ticks, low, high, style) = { - let kind = if axis.grid in (true, "major") { - 1 - } else if axis.grid == "minor" { - 2 - } else if axis.grid == "both" { - 3 - } else { - 0 - } - + let kind = _get-grid-mode(axis.grid) if kind > 0 { draw.on-layer(style.grid-layer, { for (distance, label, is-major) in ticks { @@ -338,3 +345,86 @@ }) } } + +/// Draw angular polar grid +#let draw-angular-grid(projection, ticks, style) = { + let (angular, distal, ..) = projection.axes + let mode = _get-grid-mode(distal.grid) + if mode == 0 { + return + } + + let (origin,) = (projection.transform)( + (angular.min, distal.min), + ) + + let padding = style.padding.first() + let range = angular.max - angular.min + + draw.on-layer(style.grid-layer, { + for (pos, _, is-major) in ticks { + if not _draw-grid(mode, is-major) { + continue + } + + let (pos,) = (projection.transform)( + (angular.min + pos * range, distal.max), + ) + + pos = vector.add(pos, vector.scale(vector.norm(vector.sub(pos, origin)), padding)) + + draw.line(origin, pos, + stroke: if is-major { style.grid.stroke } else { style.grid.minor-stroke }) + } + }) +} + +/// Draw distal polar grid +#let draw-distal-grid(projection, ticks, style) = { + let (angular, distal, ..) = projection.axes + let mode = _get-grid-mode(distal.grid) + if mode == 0 { + return + } + + let (origin, start, stop) = (projection.transform)( + (angular.min, distal.min), + (angular.min, distal.max), + (angular.max, distal.max), + ).map(v => v.map(calc.round.with(digits: 6))) + + let is-arc = start != stop + let radius = vector.dist(origin, start) + let range = distal.max - distal.min + + let draw-ring = (position, stroke) => { + let v = distal.min + position * range + if distal.min < v and v < distal.max { + if not is-arc { + draw.circle(origin, radius: radius / range * v, + stroke: stroke, + fill: none) + } else { + let (start, mid, stop) = (projection.transform)( + (angular.min, v), + ((angular.min + angular.max) / 2, v), + (angular.max, v) + ) + + draw.arc-through(start, mid, stop, + stroke: stroke, + fill: none) + } + } + } + + draw.on-layer(style.grid-layer, { + for (pos, _, is-major) in ticks { + if not _draw-grid(mode, is-major) { + continue + } + + draw-ring(pos, if is-major { style.grid.stroke } else { style.grid.minor-stroke }) + } + }) +} From 2b5f749a78e805b32dd2cedd4a35b541ca0b7da3 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 25 Nov 2024 02:53:02 +0100 Subject: [PATCH 06/21] repo: Remove axes.typ --- src/axes.typ | 890 --------------------------------------------------- 1 file changed, 890 deletions(-) delete mode 100644 src/axes.typ diff --git a/src/axes.typ b/src/axes.typ deleted file mode 100644 index dd01c8e..0000000 --- a/src/axes.typ +++ /dev/null @@ -1,890 +0,0 @@ -#import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process -#import "/src/plot/formats.typ" - -#let default-style = ( - tick-limit: 100, - minor-tick-limit: 1000, - auto-tick-factors: (1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10), // Tick factor to try - auto-tick-count: 11, // Number of ticks the plot tries to place - fill: none, - stroke: auto, - label: ( - offset: .2cm, // Axis label offset - anchor: auto, // Axis label anchor - angle: auto, // Axis label angle - ), - axis-layer: 0, - grid-layer: 0, - background-layer: 0, - padding: 0, - tick: ( - fill: none, - stroke: black + 1pt, - minor-stroke: black + .5pt, - offset: 0, - minor-offset: 0, - length: .1cm, // Tick length: Number - minor-length: 70%, // Minor tick length: Number, Ratio - label: ( - offset: .15cm, // Tick label offset - angle: 0deg, // Tick label angle - anchor: auto, // Tick label anchor - "show": auto, // Show tick labels for axes in use - ) - ), - break-point: ( - width: .75cm, - length: .15cm, - ), - grid: ( - stroke: (paint: gray.lighten(50%), thickness: 1pt), - ), - minor-grid: ( - stroke: (paint: gray.lighten(50%), thickness: .5pt), - ), -) - -// Default Scientific Style -#let default-style-scientific = util.merge-dictionary(default-style, ( - left: (tick: (label: (anchor: "east"))), - bottom: (tick: (label: (anchor: "north"))), - right: (tick: (label: (anchor: "west"))), - top: (tick: (label: (anchor: "south"))), - stroke: (cap: "square"), - padding: 0, -)) - -// Default Schoolbook Style -#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) -)) - -#let _prepare-style(ctx, style) = { - if type(style) != dictionary { return style } - - let res = util.resolve-number.with(ctx) - let rel-to(v, to) = { - if type(v) == ratio { - return v * to / 100% - } else { - return res(v) - } - } - - style.tick.length = res(style.tick.length) - style.tick.offset = rel-to(style.tick.offset, style.tick.length) - style.tick.minor-length = rel-to(style.tick.minor-length, style.tick.length) - style.tick.minor-offset = rel-to(style.tick.minor-offset, style.tick.minor-length) - style.tick.label.offset = res(style.tick.label.offset) - - // Break points - style.break-point.width = res(style.break-point.width) - style.break-point.length = res(style.break-point.length) - - // Padding - style.padding = res(style.padding) - - if "overshoot" in style { - style.overshoot = res(style.overshoot) - } - - return style -} - -#let _get-axis-style(ctx, style, name) = { - if not name in style { - return style - } - - style = styles.resolve(style, merge: style.at(name)) - return _prepare-style(ctx, style) -} - -#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 -} - -#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)) - } -} - -// Construct Axis Object -// -// - min (number): Minimum value -// - max (number): Maximum value -// - ticks (dictionary): Tick settings: -// - step (number): Major tic step -// - minor-step (number): Minor tic step -// - 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, - decimals: 2, grid: 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, - kind: "cartesian", -) - -// Format a tick value -#let format-tick-value(value, tic-options) = { - // Without it we get negative zero in conversion - // to content! Typst has negative zero floats. - if value == 0 { value = 0 } - - if type(value) != std.content { - let format = tic-options.at("format", default: "float") - if format == none { - value = [] - } else if type(format) == std.content { - value = format - } else if type(format) == function { - value = (format)(value) - } else if format == "sci" { - value = formats.sci(value, digits: tic-options.at("decimals", default: 2)) - } else { - value = formats.decimal(value, digits: tic-options.at("decimals", default: 2)) - } - } else if type(value) != std.content { - value = str(value) - } - - return value -} - -// Get value on axis [0, 1] -// -// - axis (axis): Axis -// - v (number): Value -// -> float -#let value-on-axis(axis, v) = { - if v == none { return } - let (min, max) = (axis.min, axis.max) - let dt = max - min; if dt == 0 { dt = 1 } - - return (v - min) / dt -} - -// Compute list of linear ticks for axis -// -// - axis (axis): Axis -#let compute-linear-ticks(axis, style, add-zero: true) = { - let (min, max) = (axis.min, axis.max) - let dt = max - min; if (dt == 0) { dt = 1 } - let ticks = axis.ticks - let ferr = util.float-epsilon - 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(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.minor-step - - let num-ticks = int(max * s + 1.5) - int(min * s) - assert(num-ticks <= minor-tick-limit, - message: "Number of minor ticks exceeds limit " + str(minor-tick-limit)) - - let n = range(int(min * s), int(max * s + 1.5)) - for t in n { - let v = (t / s - min) / dt - if v in major-tick-values { - // Prefer major ticks over minor ticks - continue - } - - if v != none and v >= 0 and v <= 1 + ferr { - l.push((v, none, false)) - } - } - } - - } - - 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 -#let fixed-ticks(axis) = { - let l = () - if "list" in axis.ticks { - for t in axis.ticks.list { - let (v, label) = (none, none) - if type(t) in (float, int) { - v = t - label = format-tick-value(t, axis.ticks) - } else { - (v, label) = t - } - - v = value-on-axis(axis, v) - if v != none and v >= 0 and v <= 1 { - l.push((v, label, true)) - } - } - } - return l -} - -// Compute list of axis ticks -// -// A tick triple has the format: -// (rel-value: float, label: content, major: bool) -// -// - axis (axis): Axis object -#let compute-ticks(axis, style, add-zero: true) = { - let find-max-n-ticks(axis, n: 11) = { - let dt = calc.abs(axis.max - axis.min) - let scale = calc.floor(calc.log(dt, base: 10) - 1) - if scale > 5 or scale < -5 {return none} - - let (step, best) = (none, 0) - for s in style.auto-tick-factors { - s = s * calc.pow(10, scale) - - let divs = calc.abs(dt / s) - if divs >= best and divs <= n { - step = s - best = divs - } - } - return step - } - - if axis == none or axis.ticks == none { return () } - if axis.ticks.step == auto { - axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count) - } - if axis.ticks.minor-step == auto { - axis.ticks.minor-step = if axis.ticks.step != none { - axis.ticks.step / 5 - } else { - none - } - } - - 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 -} - -// Prepares the axis post creation. The given axis -// must be completely set-up, including its interval. -// Returns the prepared axis -#let prepare-axis(ctx, axis, name) = { - let style = styles.resolve(ctx.style, root: "axes", - base: default-style-scientific) - style = _prepare-style(ctx, style) - style = _get-axis-style(ctx, style, name) - - if type(axis.inset) != array { - axis.inset = (axis.inset, axis.inset) - } - - axis.inset = axis.inset.map(v => util.resolve-number(ctx, v)) - - if axis.show-break { - if axis.min > 0 { - axis.inset.at(0) += style.break-point.width - } else if axis.max < 0 { - axis.inset.at(1) += style.break-point.width - } - } - - return axis -} - -// Transform a single value along a cartesian axis -#let transform-cartesian(axis, v) = { - let a = axis.origin - let b = axis.target - - let length = vector.dist(a, b) - axis.inset.sum() - let offset = 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 factor = length / (transform-func(axis.max) - transform-func(axis.min)) - return vector.scale( - vector.norm(vector.sub(b, a)), - (transform-func(v) - transform-func(axis.min)) * factor + offset) -} - -// Transform a single vector along a x, y and z axis -// -// - axes (list): List of axes -// - vec (vector): Input vector to transform -// -> vector -#let transform-vec(axes, vec) = { - let res = (0, 0, 0) - for (dim, axis) in axes.enumerate() { - let v = vec.at(dim, default: 0) - - if axis.kind == "cartesian" { - res = vector.add(res, transform-cartesian(axis, v)) - } else { - panic("Unknown axit type " + repr(axis.kind)) - } - } - return res -} - -// Draw inside viewport coordinates of two axes -// -// - axes (list): List of axes -// - name (string,none): Group name -#let axis-viewport(axes, body, name: none) = { - draw.group(name: name, (ctx => { - let transform = ctx.transform - - ctx.transform = matrix.ident() - let (ctx, drawables, bounds) = process.many(ctx, util.resolve-body(ctx, body)) - - ctx.transform = transform - - drawables = drawables.map(d => { - if "segments" in d { - d.segments = d.segments.map(((kind, ..pts)) => { - (kind, ..pts.map(pt => { - transform-vec(axes, pt) - })) - }) - } - if "pos" in d { - d.pos = transform-vec(axes, d.pos) - } - return d - }) - - return ( - ctx: ctx, - drawables: drawable.apply-transform(ctx.transform, drawables) - ) - },)) -} - -// 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) - } - } - } -} - -// Place a list of tick marks and labels along a path -#let place-ticks-on-line(ticks, start, stop, style, flip: false, is-mirror: false) = { - let dir = vector.sub(stop, start) - let norm = vector.norm((-dir.at(1), dir.at(0), dir.at(2, default: 0))) - - let def(v, d) = { - return if v == none or v == auto {d} else {v} - } - - let show-label = style.tick.label.show - if show-label == auto { - show-label = not is-mirror - } - - for (distance, label, is-major) in ticks { - let offset = style.tick.offset - let length = if is-major { style.tick.length } else { style.tick.minor-length } - if flip { - offset *= -1 - length *= -1 - } - - let pt = vector.lerp(start, stop, distance) - let a = vector.add(pt, vector.scale(norm, offset)) - let b = vector.add(a, vector.scale(norm, length)) - - draw.line(a, b, stroke: style.tick.stroke) - - if show-label and label != none { - let offset = style.tick.label.offset - if flip { - offset *= -1 - length *= -1 - } - - let c = vector.sub(if length <= 0 { b } else { a }, - vector.scale(norm, offset)) - - let angle = def(style.tick.label.angle, 0deg) - let anchor = def(style.tick.label.anchor, "center") - - draw.content(c, [#label], angle: angle, anchor: anchor) - } - } -} - -// Draw up to four axes in an "scientific" style at origin (0, 0) -// -// - size (array): Size (width, height) -// - left (axis): Left (y) axis -// - bottom (axis): Bottom (x) axis -// - right (axis): Right axis -// - top (axis): Top axis -// - name (string): Object name -// - draw-unset (bool): Draw axes that are set to `none` -// - ..style (any): Style -#let scientific(size: (1, 1), - left: none, - right: auto, - bottom: none, - top: auto, - draw-unset: true, - name: none, - ..style) = { - import draw: * - - if right == auto { - if left != none { - right = left; right.is-mirror = true - } else { - right = none - } - } - if top == auto { - if bottom != none { - top = bottom; top.is-mirror = true - } else { - top = none - } - } - - 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-scientific) - style = _prepare-style(ctx, style) - - // Compute ticks - let x-ticks = compute-ticks(bottom, style) - let y-ticks = compute-ticks(left, style) - let x2-ticks = compute-ticks(top, style) - let y2-ticks = compute-ticks(right, style) - - // Draw frame - if style.fill != none { - on-layer(style.background-layer, { - rect((0,0), (w,h), fill: style.fill, stroke: none) - }) - } - - // Draw grid - group(name: "grid", ctx => { - let axes = ( - ("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom), - ("top", (0,h), (0,0), (+w,0), x2-ticks, top), - ("left", (0,0), (w,0), (0,+h), y-ticks, left), - ("right", (w,0), (0,0), (0,+h), y2-ticks, right), - ) - for (name, start, end, direction, ticks, axis) in axes { - if axis == none { continue } - - let style = _get-axis-style(ctx, style, name) - let is-mirror = axis.at("is-mirror", default: false) - - if not is-mirror { - on-layer(style.grid-layer, { - draw-grid-lines(ctx, axis, ticks, start, end, direction, style) - }) - } - } - }) - - // Draw axes - group(name: "axes", { - let axes = ( - ("bottom", (0, 0), (w, 0), (0, -1), false, x-ticks, bottom,), - ("top", (0, h), (w, h), (0, +1), true, x2-ticks, top,), - ("left", (0, 0), (0, h), (-1, 0), true, y-ticks, left,), - ("right", (w, 0), (w, h), (+1, 0), false, y2-ticks, right,) - ) - let label-placement = ( - bottom: ("south", "north", 0deg), - top: ("north", "south", 0deg), - left: ("west", "south", 90deg), - right: ("east", "north", 90deg), - ) - - for (name, start, end, outsides, flip, ticks, axis) in axes { - let style = _get-axis-style(ctx, style, name) - let is-mirror = axis == none or axis.at("is-mirror", default: false) - let is-horizontal = name in ("bottom", "top") - - if style.padding != 0 { - let padding = vector.scale(outsides, style.padding) - start = vector.add(start, padding) - end = vector.add(end, padding) - } - - let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - - 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 { - path; - place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror) - } - }) - - if axis != none and axis.label != none and not is-mirror { - let offset = vector.scale(outsides, style.label.offset) - let (group-anchor, content-anchor, angle) = label-placement.at(name) - - if style.label.anchor != auto { - content-anchor = style.label.anchor - } - if style.label.angle != auto { - angle = style.label.angle - } - - content((rel: offset, to: "axis." + group-anchor), - [#axis.label], - angle: angle, - anchor: content-anchor) - } - }) - } - }) - }) -} - -// 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) == std.content { - style.shared-zero - } else { - $0$ - } - content(pt, zero, anchor: "north-east") - } - }) - }) - }) -} From 39ef220474ffa24639f05fd9cdb7597284f1777a Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 25 Nov 2024 11:27:31 +0100 Subject: [PATCH 07/21] WIP change ticks --- src/axis.typ | 13 ++-- src/plot.typ | 12 ++-- src/plot/util.typ | 35 ++++++++- src/spine.typ | 136 +++++++++++++++++++++-------------- src/ticks.typ | 179 ++++++++++++++++++++-------------------------- 5 files changed, 211 insertions(+), 164 deletions(-) diff --git a/src/axis.typ b/src/axis.typ index 3d22444..378b507 100644 --- a/src/axis.typ +++ b/src/axis.typ @@ -20,30 +20,33 @@ } /// Linear Axis Constructor -#let linear(name, min, max) = ( +#let linear(name, min, max, ..options) = ( label: [#name], name: name, min: min, max: max, base: 10, transform: _transform-lin, auto-domain: (none, none), ticks: (step: auto, minor-step: none, format: auto, list: none), grid: none, compute-ticks: ticks.compute-ticks.with("lin"), -) +) + options.named() /// Log Axis Constructor -#let logarithmic(name, min, max, base) = ( +#let logarithmic(name, min, max, base, ..options) = ( label: [#name], name: name, min: min, max: max, base: base, transform: _transform-log, auto-domain: (none, none), ticks: (step: auto, minor-step: none, format: auto, list: none), grid: none, compute-ticks: ticks.compute-ticks.with("log"), -) +) + options.named() // Prepare axis #let prepare(ptx, ax) = { if ax.min == none { ax.min = ax.auto-domain.at(0) } if ax.max == none { ax.max = ax.auto-domain.at(1) } - if "compute-ticks" in ax { ax.computed-ticks = (ax.compute-ticks)(ax) } + if ax.min == none or ax.max == none { ax.min = -1e-6; ax.max = +1e-6 } + if "compute-ticks" in ax { + ax.computed-ticks = (ax.compute-ticks)(ax) + } return ax } diff --git a/src/plot.typ b/src/plot.typ index 5e3da4f..c1e5b98 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -39,9 +39,9 @@ /// - name (str): Axis name /// - min: (none, float): Minimum /// - max: (none, float): Maximum -#let lin-axis(name, min: none, max: none) = { +#let lin-axis(name, min: none, max: none, ..options) = { ((priority: -100, fn: (ptx) => { - ptx.axes.insert(name, axis.linear(name, min, max)) + ptx.axes.insert(name, axis.linear(name, min, max, ..options)) return ptx }),) } @@ -51,9 +51,9 @@ /// - min: (none, float): Minimum /// - max: (none, float): Maximum /// - base: (int): Log base -#let log-axis(name, min: none, max: none, base: 10) = { +#let log-axis(name, min: none, max: none, base: 10, ..options) = { ((priority: -100, fn: (ptx) => { - ptx.axes.insert(name, axis.logarithmic(name, min, max, base)) + ptx.axes.insert(name, axis.logarithmic(name, min, max, base, ..options)) return ptx }),) } @@ -63,7 +63,9 @@ scientific: (ptx) => { lin-axis("x") lin-axis("y") - sub-plot.new("x", "y") + lin-axis("u") + lin-axis("v") + sub-plot.new("x", "y", "u", "v") }, school-book: (ptx) => { lin-axis("x") diff --git a/src/plot/util.typ b/src/plot/util.typ index 30830f2..de28264 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -204,8 +204,24 @@ /// - axes (list): List of axes /// -> array List of stroke paths #let compute-stroke-paths(points, axes) = { + if not axes.any(ax => ax.at("clip", default: true)) { + return (points,) + } + let (x, y, ..) = axes - clipped-paths(points, (x.min, y.min), (x.max, y.max), fill: false) + let (x-min, x-max) = if x.at("clip", default: true) { + (x.min, x.max) + } else { + (-float.inf, float.inf) + } + + let (y-min, y-max) = if y.at("clip", default: true) { + (y.min, y.max) + } else { + (-float.inf, float.inf) + } + + clipped-paths(points, (x-min, y-min), (x-max, y-max), fill: false) } /// Compute clipped fill path @@ -214,7 +230,23 @@ /// - axes (list): List of axes /// -> array List of fill paths #let compute-fill-paths(points, axes) = { + if not axes.any(ax => ax.at("clip", default: true)) { + return (points,) + } + let (x, y, ..) = axes + let (x-min, x-max) = if x.at("clip", default: true) { + (x.min, x.max) + } else { + (-float.inf, float.inf) + } + + let (y-min, y-max) = if y.at("clip", default: true) { + (y.min, y.max) + } else { + (-float.inf, float.inf) + } + clipped-paths(points, (x.min, y.min), (x.max, y.max), fill: true) } @@ -354,6 +386,7 @@ for (name, ax) in axes { ax.min = get-opt(name, "min", ax.min) ax.max = get-opt(name, "max", ax.max) + ax.clip = get-opt(name, "clip", ax.at("clip", default: true)) ax.label = get-opt(name, "label", ax.label) ax.transform = get-opt(name, "transform", ax.transform) ax.ticks.list = get-opt(name, "list", ax.ticks.list) diff --git a/src/spine.typ b/src/spine.typ index a88a92e..15e7ce6 100644 --- a/src/spine.typ +++ b/src/spine.typ @@ -3,6 +3,7 @@ #import "/src/ticks.typ" #import "/src/projection.typ" +#import "/src/axis.typ" /// Default axis style /// @@ -85,21 +86,19 @@ ), y: ( tick: ( - flip: true, label: ( anchor: "east", ), ), ), - x2: ( + u: ( tick: ( - flip: true, label: ( anchor: "south", ), ), ), - y2: ( + v: ( tick: ( label: ( anchor: "west", @@ -108,7 +107,6 @@ ), distal: ( tick: ( - flip: true, label: ( anchor: "east", ) @@ -160,73 +158,97 @@ #let _get-axis-style(ptx, style, name) = { return _prepare-style(ptx, if name in style { - cetz.util.merge-dictionary(style, style.at(name)) + cetz.util.merge-dictionary(style, style.at(name, default: (:))) } else { style }) } +/// +#let cartesian-axis-projection(ax, start, stop) = { + let dir = vector.norm(vector.sub(stop, start)) + let dist = vector.dist(start, stop) + return (value) => { + vector.add(start, vector.scale(dir, axis.transform(ax, value, 0, dist))) + } +} + /// #let cartesian-scientific(projections: none, name: none, style: (:)) = { return ( name: name, draw: (ptx) => { - let proj = projections.at(0) - let axes = proj.axes - let x = axes.at(0) - let y = axes.at(1) + let xy-proj = projections.at(0) + let uv-proj = projections.at(1, default: xy-proj) + let has-uv = projections.len() > 1 + let (x, y) = xy-proj.axes + let (u, v) = uv-proj.axes let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, root: "axes", merge: style, base: default-style)) let x-style = _get-axis-style(ptx, style, "x") let y-style = _get-axis-style(ptx, style, "y") - let x2-style = _get-axis-style(ptx, style, "x2") - let y2-style = _get-axis-style(ptx, style, "y2") + let u-style = _get-axis-style(ptx, style, "u") + let v-style = _get-axis-style(ptx, style, "v") - let (south-west, south-east, north-west, north-east) = (proj.transform)( + let (x-low, x-high, y-low, y-high) = (xy-proj.transform)( (x.min, y.min), (x.max, y.min), - (x.min, y.max), (x.max, y.max), + (x.min, y.min), (x.min, y.max), + ) + let (u-low, u-high, v-low, v-high) = (uv-proj.transform)( + (u.min, v.max), (u.max, v.max), + (u.max, v.min), (u.max, v.max), ) - let x-padding = x-style.padding - let y-padding = y-style.padding - - let x-low = vector.add(south-west, (-x-padding.at(0), -y-padding.at(0))) - let x-high = vector.add(south-east, (+x-padding.at(1), -y-padding.at(0))) - let y-low = vector.add(south-west, (-x-padding.at(0), -y-padding.at(0))) - let y-high = vector.add(north-west, (-x-padding.at(0), y-padding.at(1))) + let move-vec(v, direction, length) = { + vector.add(v, direction.enumerate().map(((i, v)) => v * length.at(i))) + } - let x2-low = vector.add(north-west, (-x-padding.at(0), y-padding.at(1))) - let x2-high = vector.add(north-east, (+x-padding.at(1), y-padding.at(1))) - let y2-low = vector.add(south-east, (x-padding.at(1), -y-padding.at(0))) - let y2-high = vector.add(north-east, (x-padding.at(1), y-padding.at(1))) + // Outset axes + x-low = move-vec(x-low, (0, -1), x-style.padding) + x-high = move-vec(x-high, (0, -1), x-style.padding) + y-low = move-vec(y-low, (-1, 0), y-style.padding) + y-high = move-vec(y-high, (-1, 0), y-style.padding) + u-low = move-vec(u-low, (0, 1), u-style.padding) + u-high = move-vec(u-high, (0, 1), u-style.padding) + v-low = move-vec(v-low, (1, 0), v-style.padding) + v-high = move-vec(v-high, (1, 0), v-style.padding) + + // Frame corners (FIX for uv axes) + let south-west = move-vec(x-low, (-1, 0), x-style.padding) + let south-east = move-vec(x-high, (+1, 0), x-style.padding) + let north-west = move-vec(u-low, (-1, 0), u-style.padding) + let north-east = move-vec(u-high, (+1, 0), u-style.padding) + + // Grid lengths + let x-grid-length = u-low.at(1) - x-low.at(1) + let y-grid-length = v-low.at(0) - y-low.at(0) + let u-grid-length = x-low.at(1) - u-low.at(1) + let v-grid-length = y-low.at(0) - v-low.at(0) let axes = ( - (x, 0, south-west, south-east, x-low, x-high, x-style, false), - (y, 1, south-west, north-west, y-low, y-high, y-style, false), - (x, 0, north-west, north-east, x2-low, x2-high, x2-style, true), - (y, 1, south-east, north-east, y2-low, y2-high, y2-style, true), + (x, (0,+1), (0,x-grid-length), cartesian-axis-projection(x, x-low, x-high), x-style, false), + (y, (+1,0), (y-grid-length,0), cartesian-axis-projection(y, y-low, y-high), y-style, false), + (u, (0,-1), (0,u-grid-length), cartesian-axis-projection(u, u-low, u-high), u-style, not has-uv), + (v, (-1,0), (v-grid-length,0), cartesian-axis-projection(v, v-low, v-high), v-style, not has-uv), ) - for (ax, component, low, high, frame-low, frame-high, style, mirror) in axes { - draw.on-layer(style.axis-layer, { - draw.line(frame-low, frame-high, stroke: style.stroke, mark: style.mark) - }) - if "computed-ticks" in ax { - let low = low.enumerate().map(((i, v)) => { - if i == component { v } else { frame-low.at(i) } + draw.group(name: "spine", { + for (ax, dir, grid-dir, proj, style, mirror) in axes { + draw.on-layer(style.axis-layer, { + draw.line(proj(ax.min), proj(ax.max), stroke: style.stroke, mark: style.mark) }) - let high = high.enumerate().map(((i, v)) => { - if i == component { v } else { frame-low.at(i) } - }) - - if not mirror { - ticks.draw-cartesian-grid(low, high, component, ax, ax.computed-ticks, (0,0), (1,0), style) + if "computed-ticks" in ax { + if not mirror { + ticks.draw-cartesian-grid(proj, grid-dir, ax, ax.computed-ticks, style) + } + ticks.draw-cartesian(proj, dir, ax.computed-ticks, style, is-mirror: mirror) } - ticks.draw-cartesian(low, high, ax.computed-ticks, style, is-mirror: mirror) } - } + }) + + // TODO: Draw labels }, ) } @@ -247,10 +269,17 @@ let x-style = _get-axis-style(ptx, style, "x") let y-style = _get-axis-style(ptx, style, "y") + let zero-x = calc.max(x.min, calc.min(0, x.max)) + let zero-y = calc.max(y.min, calc.min(0, y.max)) + let zero-pt = ( + calc.max(x.min, calc.min(zero.at(0), x.max)), + calc.max(y.min, calc.min(zero.at(1), y.max)), + ) + let (zero, min-x, max-x, min-y, max-y) = (proj.transform)( - zero, - vector.add(zero, (x.min, 0)), vector.add(zero, (x.max, 0)), - vector.add(zero, (0, y.min)), vector.add(zero, (0, y.max)), + zero-pt, + vector.add(zero-pt, (x.min, zero-y)), vector.add(zero-pt, (x.max, zero-y)), + vector.add(zero-pt, (zero-x, y.min)), vector.add(zero-pt, (zero-x, y.max)), ) let x-padding = x-style.padding @@ -272,6 +301,7 @@ let outset-min-y = vector.scale(outset-lo-y, -1) let outset-max-y = vector.scale(outset-hi-y, +1) + draw.on-layer(x-style.axis-layer, { draw.line((rel: outset-min-x, to: min-x), (rel: outset-max-x, to: max-x), @@ -279,8 +309,9 @@ stroke: x-style.stroke) }) if "computed-ticks" in x { - ticks.draw-cartesian-grid(min-x, max-x, 0, x, x.computed-ticks, min-y, max-y, x-style) - ticks.draw-cartesian(min-x, max-x, x.computed-ticks, x-style) + //ticks.draw-cartesian-grid(grid-proj, grid-dir, ax, ax.computed-ticks, style) + let tick-proj = cartesian-axis-projection(x, min-x, max-x) + ticks.draw-cartesian(tick-proj, (0,+1), x.computed-ticks, x-style) } draw.on-layer(y-style.axis-layer, { @@ -290,8 +321,9 @@ stroke: y-style.stroke) }) if "computed-ticks" in y { - ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) - ticks.draw-cartesian(min-y, max-y, y.computed-ticks, y-style) + //ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) + let tick-proj = cartesian-axis-projection(y, min-y, max-y) + ticks.draw-cartesian(tick-proj, (+1,0), y.computed-ticks, y-style) } } ) @@ -332,7 +364,7 @@ if "computed-ticks" in distal { // TODO ticks.draw-distal-grid(proj, distal.computed-ticks, distal-style) - ticks.draw-cartesian(r-start, r-end, distal.computed-ticks, distal-style) + //ticks.draw-cartesian(r-start, r-end, distal.computed-ticks, distal-style) } if start == stop { diff --git a/src/ticks.typ b/src/ticks.typ index 8130192..9e3f380 100644 --- a/src/ticks.typ +++ b/src/ticks.typ @@ -45,77 +45,81 @@ return value } -// Compute list of linear ticks for axis -// -// - axis (axis): Axis -#let compute-linear-ticks(axis, add-zero: true) = { +#let clip-ticks(axis, ticks) = { let (min, max) = (axis.min, axis.max) - let dt = max - min; if (dt == 0) { dt = 1 } - let ticks = axis.ticks - let ferr = util.float-epsilon - let tick-limit = axis.at("tick-limit", default: 100) - let minor-tick-limit = axis.at("minor-tick-limit", default: 1000) + let err = util.float-epsilon + /* + return ticks.filter(((value, ..)) => { + min - err <= value and value <= max + err + }) + */ + return ticks +} - 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 } +/// Compute list of linear ticks +/// +/// - ax (axis): Axes +/// -> List of ticks +#let compute-linear-ticks(ax) = { + let compute-list(min, max, step, limit) = { + if step == none or step <= 0 or min == none or max == none { + return () + } - let s = 1 / ticks.step + let num-negative = int((0 - min) / step) + let num-positive = int((max - 0) / 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)) + return range(-num-negative, num-positive + 1).map(t => { + t * step + }) + } - 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 } + let major-limit = ax.at("tick-limit", default: 100) + let minor-limit = ax.at("minor-tick-limit", default: 1000) - if v >= 0 - ferr and v <= 1 + ferr { - l.push((v, format-tick-value(t / s, ticks), true)) - major-tick-values.push(v) - } - } - } + let major = compute-list(ax.min, ax.max, ax.ticks.step, major-limit) + let minor = compute-list(ax.min, ax.max, ax.ticks.minor-step, minor-limit) - 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.minor-step + minor.map(value => { + (value, none, false) + }) + major.map(value => { + (value, format-tick-value(value, ax.ticks), true) + }) +} - let num-ticks = int(max * s + 1.5) - int(min * s) - assert(num-ticks <= minor-tick-limit, - message: "Number of minor ticks exceeds limit " + str(minor-tick-limit)) +/// Compute list of logarithmic ticks +/// +/// - ax (axis): Axis +/// -> List of ticks +#let compute-logarithmic-ticks(ax) = { + let min = calc.log(calc.max(axis.min, util.float-epsilon), base: ax.base) + let max = calc.log(calc.max(axis.max, util.float-epsilon), base: ax.base) - let n = range(int(min * s), int(max * s + 1.5)) - for t in n { - let v = (t / s - min) / dt - if v in major-tick-values { - // Prefer major ticks over minor ticks - continue - } + let compute-list(min, max, step, limit) = { + let num-positive = int((max - 0) / step) - if v != none and v >= 0 and v <= 1 + ferr { - l.push((v, none, false)) - } - } - } + // TODO + return () } - return l + let major-limit = ax.at("tick-limit", default: 100) + let minor-limit = ax.at("minor-tick-limit", default: 1000) + + let major = compute-list(ax.min, ax.max, ax.ticks.step, major-limit) + let minor = compute-list(ax.min, ax.max, ax.ticks.minor-step, minor-limit) + + minor.map(value => { + (value, none, false) + }) + major.map(value => { + (value, format-tick-value(value, ax.ticks), true) + }) } // Compute list of linear ticks for axis // // - axis (axis): Axis -#let compute-logarithmic-ticks(axis, add-zero: true) = { +#let compute-logarithmic-ticks__(axis, add-zero: true) = { let ferr = util.float-epsilon let (min, max) = ( calc.log(calc.max(axis.min, ferr), base: axis.base), @@ -141,20 +145,6 @@ 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 { @@ -205,11 +195,10 @@ (v, label) = t } - v = value-on-axis(axis, v) - if v != none and v >= 0 and v <= 1 { - l.push((v, label, true)) + if v != none { + return (v, label, true) } - }) + }).filter(v => v != none) } // Compute list of axis ticks @@ -219,12 +208,16 @@ // // - mode (str): "lin" or "log" // - axis (axis): Axis object -#let compute-ticks(mode, axis, add-zero: true) = { +#let compute-ticks(mode, axis) = { let auto-tick-count = 11 let auto-tick-factors = (1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10) - let find-max-n-ticks(axis, n: 11) = { - let dt = calc.abs(axis.max - axis.min) + let find-max-n-ticks(ax, n: 11) = { + if ax.min == none or ax.max == none { + return none + } + + let dt = calc.abs(ax.max - ax.min) let scale = calc.floor(calc.log(dt, base: 10) - 1) if scale > 5 or scale < -5 {return none} @@ -254,22 +247,19 @@ } let ticks = if mode == "log" { - compute-logarithmic-ticks(axis, add-zero: add-zero) + compute-logarithmic-ticks(axis) } else { - compute-linear-ticks(axis, add-zero: add-zero) + compute-linear-ticks(axis) } ticks += fixed-ticks(axis) return ticks } // Place a list of tick marks and labels along a line -#let draw-cartesian(start, stop, ticks, style, is-mirror: false, show-zero: true) = { +#let draw-cartesian(transform, norm, ticks, style, is-mirror: false, show-zero: true) = { let draw-label = style.tick.label.draw draw.on-layer(style.tick-layer, { - let dir = vector.norm(vector.sub(stop, start)) - let norm = (-dir.at(1), dir.at(0), dir.at(2, default: 0)) - let def(v, d) = { return if v == none or v == auto {d} else {v} } @@ -279,12 +269,12 @@ show-label = not is-mirror } - for (distance, label, is-major) in ticks { + for (value, label, is-major) in ticks { let offset = if is-major { style.tick.offset } else { style.tick.minor-offset } let length = if is-major { style.tick.length } else { style.tick.minor-length } let stroke = if is-major { style.tick.stroke } else { style.tick.minor-stroke } - let pt = vector.lerp(start, stop, distance) + let pt = transform(value) if style.tick.flip { offset = -offset - length } @@ -312,33 +302,20 @@ // Draw grid lines for the ticks of an axis // -// - ptx (context): Plot context -// - start (vector): Axis start -// - stop (vector): Axis stop -// - component (int): Vector compontent to use as direction -// - 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 -// - style (style): Style -#let draw-cartesian-grid(start, stop, component, axis, ticks, low, high, style) = { +#let draw-cartesian-grid(proj, offset, axis, ticks, style) = { let kind = _get-grid-mode(axis.grid) if kind > 0 { draw.on-layer(style.grid-layer, { - for (distance, label, is-major) in ticks { - let offset = vector.lerp(start, stop, distance) - - let start = low - start.at(component) = offset.at(component) - let end = high - end.at(component) = offset.at(component) + for (value, _, major) in ticks { + let start = proj(value) + let end = vector.add(start, offset) // Draw a minor line - if not is-major and kind >= 2 { + if not major and kind >= 2 { draw.line(start, end, stroke: style.grid.minor-stroke) } // Draw a major line - if is-major and (kind == 1 or kind == 3) { + if major and (kind == 1 or kind == 3) { draw.line(start, end, stroke: style.grid.stroke) } } From f6d163b182915273fae45fab1c8e086c5997f54c Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 26 Nov 2024 02:32:16 +0100 Subject: [PATCH 08/21] WIP Tests --- src/plot.typ | 18 +++++-- src/plot/legend.typ | 2 +- src/plot/util.typ | 47 ++--------------- src/ticks.typ | 16 +++++- tests/axes/log-mode/test.typ | 60 +++++++++------------- tests/plot/equal-axis/test.typ | 8 +-- tests/plot/marks/test.typ | 2 +- tests/plot/test.typ | 93 +++++++++++++++------------------- 8 files changed, 105 insertions(+), 141 deletions(-) diff --git a/src/plot.typ b/src/plot.typ index c1e5b98..7c0fcf9 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -7,6 +7,7 @@ #import "/src/spine.typ" #import "/src/ticks.typ" #import "/src/sub-plot.typ" +#import "/src/compat.typ" #import "/src/plot/sample.typ": sample-fn, sample-fn2 #import "/src/plot/line.typ": add, add-hline, add-vline, add-fill-between @@ -256,18 +257,24 @@ ) if template != none and template in templates { - body += (templates.at(template))(ptx) + body = (templates.at(template))(ptx) + body } + // Wrap old style elements + body = body.map(elem => { + return if "type" in elem { + compat.wrap(elem) + } else { + elem + } + }) + let plot-elements = body .filter(elem => type(elem) == dictionary) .sorted(key: elem => elem.at("priority", default: 0)) let cetz-elements = body .filter(elem => type(elem) == function) - // Create axes - //ptx = plot-util.create-axes(ptx, plot-elements, options.named()) - for elem in plot-elements.filter(elem => elem.priority <= 0) { assert("fn" in elem, message: "Invalid plot element: " + repr(elem)) @@ -353,11 +360,13 @@ if ptx.legend != none { draw.scope({ + /* draw.set-origin("plot." + options.at("legend", default: "north-east")) draw.group(name: "legend", anchor: options.at("legend-anchor", default: "north-west"), { draw.anchor("default", (0,0)) draw-legend(ptx) }) + */ }) } @@ -400,6 +409,7 @@ for plot in ptx.plots { for proj in plot.projections { if axes.all(name => proj.axes.contains(name)) { + // FIXME: Broken let pt = (proj.transform)(position).first() ptx.anchors.push((name, pt)) } diff --git a/src/plot/legend.typ b/src/plot/legend.typ index 859504b..03091ca 100644 --- a/src/plot/legend.typ +++ b/src/plot/legend.typ @@ -243,5 +243,5 @@ // TODO: Stub #let draw-legend(ptx) = { - draw.rect((0,0), (1,1)) + //draw.rect((0,0), (1,1)) } diff --git a/src/plot/util.typ b/src/plot/util.typ index de28264..faf9895 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -247,7 +247,7 @@ (-float.inf, float.inf) } - clipped-paths(points, (x.min, y.min), (x.max, y.max), fill: true) + clipped-paths(points, (x-min, y-min), (x-max, y-max), fill: true) } /// Return points of a sampled catmull-rom through the @@ -328,34 +328,6 @@ return pts } -// Get the default axis orientation -// depending on the axis name -#let get-default-axis-horizontal(name) = { - return lower(name).starts-with("x") -} - -// Create axes specified by options -#let create-axes(ptx, elements, options) = { - import "/src/axis.typ" - for element in elements { - if "axes" in element { - for name in element.axes { - if not name in ptx.axes { - let mode = options.at(name + "-mode", default: "lin") - - ptx.axes.insert(name, if mode == "log" { - axis.logarithmic(name, none, none, 10) - } else { - axis.linear(name, none, none) - }) - } - } - } - } - - return ptx -} - // Setup axes dictionary // // - axis-dict (dictionary): Existing axis dictionary @@ -370,19 +342,6 @@ if v == auto { default } else { v } } - // Mode switching - for (name, ax) in axes { - let mode = get-opt(name, "mode", "lin") - if mode == "lin" { - ax.transform = axis._transform-lin - } else if mode == "log" { - ax.transform = axis._transform-log - ax.base = get-opt(name, "base", ax.at("base", default: 10)) - } else { - panic("Invalid axis mode: " + repr(mode)) - } - } - for (name, ax) in axes { ax.min = get-opt(name, "min", ax.min) ax.max = get-opt(name, "max", ax.max) @@ -395,6 +354,10 @@ ax.ticks.minor-step = get-opt(name, "minor-tick-step", ax.ticks.minor-step) ax.grid = get-opt(name, "grid", ax.grid) + if get-opt(name, "mode", none) != none { + panic("Mode switching is no longer supported. Use log-axis/lin-axis to create the axis.") + } + axes.at(name) = axis.prepare(ptx, ax) } diff --git a/src/ticks.typ b/src/ticks.typ index 9e3f380..e71a489 100644 --- a/src/ticks.typ +++ b/src/ticks.typ @@ -69,6 +69,10 @@ let num-negative = int((0 - min) / step) let num-positive = int((max - 0) / step) + if num-negative + num-positive > limit { + panic("Tick limit reached! Adjust axis ticks.limit or ticks.minor-limit.") + } + return range(-num-negative, num-positive + 1).map(t => { t * step }) @@ -92,12 +96,20 @@ /// - ax (axis): Axis /// -> List of ticks #let compute-logarithmic-ticks(ax) = { - let min = calc.log(calc.max(axis.min, util.float-epsilon), base: ax.base) - let max = calc.log(calc.max(axis.max, util.float-epsilon), base: ax.base) + let min = calc.log(calc.max(ax.min, util.float-epsilon), base: ax.base) + let max = calc.log(calc.max(ax.max, util.float-epsilon), base: ax.base) let compute-list(min, max, step, limit) = { + if step == none or step <= 0 or min == none or max == none { + return () + } + let num-positive = int((max - 0) / step) + if num-positive > limit { + panic("Tick limit reached! Adjust axis ticks.limit or ticks.minor-limit.") + } + // TODO return () diff --git a/tests/axes/log-mode/test.typ b/tests/axes/log-mode/test.typ index 0860810..37bcdaf 100644 --- a/tests/axes/log-mode/test.typ +++ b/tests/axes/log-mode/test.typ @@ -3,33 +3,29 @@ #import "/tests/helper.typ": * #import "/src/lib.typ": * #import cetz: draw, canvas -#import cetz-plot: axes, #test-case({ - 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.log-axis("y", base: 10) + 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 $, ) @@ -37,6 +33,24 @@ ) }) +// Column chart test +#test-case({ + plot.plot( + size: (9, 6), + y-format: "sci", + x-min: -0.5, x-max: 4.5, x-tick-step: 1, + y-min: 0.1, y-max: 10000, step: 1, minor-step: 1, + x-grid: "both", + y-grid: "both", + { + plot.log-axis("y") + plot.add-bar( + (1, 10, 100, 1000, 10000).enumerate().map(((x,y))=>{(x,y)}), + bar-width: 0.8, + ) + } + ) +}) // Bode plot test #box(stroke: 2pt + red,{ canvas({ @@ -47,7 +61,6 @@ ) 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, @@ -68,7 +81,6 @@ ) 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$)], @@ -83,27 +95,6 @@ }) }) -// 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({ @@ -111,7 +102,6 @@ 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, @@ -135,21 +125,21 @@ } ) })) +*/ // Box plot test -#box(stroke: 2pt + red, canvas({ +#test-case({ 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.log-axis("y") plot.add-boxwhisker( ( (x: 0, min: 1, q1: 10, q2: 100, q3: 1000, max: 10000), @@ -159,4 +149,4 @@ ) } ) -})) +}) diff --git a/tests/plot/equal-axis/test.typ b/tests/plot/equal-axis/test.typ index a9b0a8f..b81b05e 100644 --- a/tests/plot/equal-axis/test.typ +++ b/tests/plot/equal-axis/test.typ @@ -12,8 +12,8 @@ x-equal: "y", b-equal: "a", { - plot.add-cartesian-axis("a", (0,0), (6,0)) - plot.add-cartesian-axis("b", (0,0), (0,3)) + plot.lin-axis("a") + plot.lin-axis("b") plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t))) plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t)), axes: ("a", "b")) @@ -29,8 +29,8 @@ x-equal: "y", b-equal: "a", { - plot.add-cartesian-axis("a", (0,0), (3,0)) - plot.add-cartesian-axis("b", (0,0), (0,6)) + plot.lin-axis("a") + plot.lin-axis("b") plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t))) plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t)), axes: ("a", "b")) diff --git a/tests/plot/marks/test.typ b/tests/plot/marks/test.typ index aa584f0..ed144de 100644 --- a/tests/plot/marks/test.typ +++ b/tests/plot/marks/test.typ @@ -7,7 +7,7 @@ #test-case({ import cetz-plot: plot - let axis-options = (("x", "y"), ("x2", "y"), ("x", "y2"), ("x2", "y2")) + let axis-options = (("x", "y"), ("u", "y"), ("x", "v"), ("u", "v")) plot.plot( size: (5,5), diff --git a/tests/plot/test.typ b/tests/plot/test.typ index 4cf0944..a3f3d2b 100644 --- a/tests/plot/test.typ +++ b/tests/plot/test.typ @@ -28,26 +28,26 @@ x-min: -360, x-max: 360, y-tick-step: 1, - x2-label: none, - x2-min: -90, - x2-max: 90, - x2-tick-step: 45, - x2-minor-tick-step: 15, - y2-label: none, - y2-min: -1.5, - y2-max: 1.5, - y2-tick-step: .5, - y2-minor-tick-step: .1, + u-label: none, + u-min: -90, + u-max: 90, + u-tick-step: 45, + u-minor-tick-step: 15, + v-label: none, + v-min: -1.5, + v-max: 1.5, + v-tick-step: .5, + v-minor-tick-step: .1, { plot.add(data) - plot.add(data, style: (stroke: blue), axes: ("x2", "y2")) + plot.add(data, style: (stroke: blue), axes: ("u", "v")) }) }) /* School-Book Style */ #test-case({ plot.plot(size: (5, 4), - axis-style: "school-book", + template: "school-book", x-tick-step: 180, y-tick-step: 1, { @@ -58,7 +58,7 @@ /* Clipping */ #test-case({ plot.plot(size: (5, 4), - axis-style: "school-book", + template: "school-book", x-min: auto, x-max: 350, x-tick-step: 180, @@ -75,7 +75,6 @@ plot.plot(size: (5, 4), x-label: [Rainbow], x-tick-step: none, - axis-style: "scientific", y-label: [Color], y-max: 8, y-tick-step: none, @@ -92,34 +91,33 @@ /* Tick Step Calculation */ #test-case({ plot.plot(size: (12, 4), - y2-decimals: 4, + v-format: plot.formats.decimal.with(digits: 4), { plot.add(((0,0), (1,10)), axes: ("x", "y")) - plot.add(((0,0), (.1,.01)), axes: ("x2", "y2")) + plot.add(((0,0), (.1,.01)), axes: ("u", "v")) }) }) #test-case({ plot.plot(size: (12, 4), - y2-decimals: 9, - x2-decimals: 9, - y2-format: "sci", + v-format: plot.formats.sci, + u-format: plot.formats.sci, { plot.add(((0,0), (30,2500)), axes: ("x", "y")) - plot.add(((0,0), (.001,.0001)), axes: ("x2", "y2")) + plot.add(((0,0), (.001,.0001)), axes: ("u", "v")) }) }) -/* Axis Styles */ - - +/* Templates */ #test-case(args => { plot.plot(size: (4,4), x-tick-step: 90, y-tick-step: 1, - axis-style: args, { + template: args, { plot.add(domain: (0, 360), x => calc.sin(x * 1deg)) }) }, args: ( - "scientific", "scientific-auto", "left", "school-book", none + // TODO + //"scientific", "scientific-auto", "left", "school-book", none + "scientific", "school-book" )) /* Manual Axis Bounds */ @@ -136,10 +134,10 @@ yb-min: -1.5, yb-max: .5, yt-min: -.5, yt-max: 1.5, { - plot.add-cartesian-axis("xl", (0, 0), (4, 0)) - plot.add-cartesian-axis("xr", (0, 4), (4, 4)) - plot.add-cartesian-axis("yt", (0, 0), (0, 4)) - plot.add-cartesian-axis("yb", (4, 0), (4, 4)) + plot.lin-axis("xl") + plot.lin-axis("xr") + plot.lin-axis("yt") + plot.lin-axis("yb") plot.add(circle-data) plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) @@ -159,10 +157,10 @@ yb-min: -1.75, yb-max: .25, yt-min: -.25, yt-max: 1.75, { - plot.add-cartesian-axis("xl", (0, 0), (4, 0)) - plot.add-cartesian-axis("xr", (0, 4), (4, 4)) - plot.add-cartesian-axis("yt", (0, 0), (0, 4)) - plot.add-cartesian-axis("yb", (4, 0), (4, 4)) + plot.lin-axis("xl") + plot.lin-axis("xr") + plot.lin-axis("yt") + plot.lin-axis("yb") plot.add(circle-data) plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) @@ -198,16 +196,6 @@ mark: (start: ">", end: ">"), name: "amplitude") }) -/* Custom sample points */ -#test-case({ - plot.plot(size: (6, 4), y-min: -2, y-max: 2, - samples: 10, - { - plot.add(samples: 2, sample-at: (.99, 1.001, 1.99, 2.001, 2.99), domain: (0, 3), - x => calc.pow(-1, int(x))) - }) -}) - /* Format tick values */ #test-case({ plot.plot(size: (6, 4), @@ -217,19 +205,20 @@ y-tick-step: none, y-ticks: (-1, 0, 1), y-format: x => $y_(#x)$, - x2-tick-step: none, - x2-ticks: (-1, 0, 1), - x2-format: x => $x_(2,#x)$, - y2-tick-step: none, - y2-ticks: (-1, 0, 1), - y2-format: x => $y_(2,#x)$, + u-tick-step: none, + u-ticks: (-1, 0, 1), + u-format: x => $x_(2,#x)$, + v-tick-step: none, + v-ticks: (-1, 0, 1), + v-format: x => $y_(2,#x)$, { plot.add(samples: 2, domain: (-1, 1), x => -x, axes: ("x", "y")) - plot.add(samples: 2, domain: (-1, 1), x => x, axes: ("x2", "y2")) + plot.add(samples: 2, domain: (-1, 1), x => x, axes: ("u", "v")) }) }) // Test plot with anchors only +/* #test-case({ import draw: * @@ -256,13 +245,13 @@ tick: ( length: -.1, ), - left: ( + y: ( stroke: (paint: red), tick: ( stroke: auto, ) ), - bottom: ( + x: ( stroke: (paint: blue, thickness: 2pt), tick: ( stroke: auto, From 24851ddefee536e1dbee958627489e971aa7d0a4 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 26 Nov 2024 04:03:21 +0100 Subject: [PATCH 09/21] compat: Add compat mode --- src/compat.typ | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/compat.typ diff --git a/src/compat.typ b/src/compat.typ new file mode 100644 index 0000000..84351db --- /dev/null +++ b/src/compat.typ @@ -0,0 +1,67 @@ +#import "/src/plot/util.typ" +#import "/src/cetz.typ" +#import cetz: draw + +#let make-cptx(ptx, old) = { + let axes = old.axes.map(name => ptx.axes.at(name)) + return ( + axes: axes, + ) +} + +#let draw-old(ptx, transform, body) = { + if body != none { + (ctx => { + ctx.transform = ((1, 0, 0, 0), + (0,-1, 0, 0), + (0, 0, 1, 0), + (0, 0, 0, 1)) + let (ctx: _, drawables, bounds) = cetz.process.many(ctx, body) + drawables = cetz.drawable.apply-transform(v => { + let (x, y) = transform(v.slice(0, 2)).first() + return (x, y, 0) + }, drawables) + + return ( + ctx: ctx, + drawables: drawables, + ) + },) + } +} + +#let wrap(old) = { + return ( + priority: 0, + fn: ptx => { + let old = old + if "plot-prepare" in old { + old = (old.plot-prepare)(old, make-cptx(ptx, old)) + } + if "x-domain" in old { + ptx = util.set-auto-domain(ptx, (old.axes.at(0),), (old.x-domain,)) + } + if "y-domain" in old { + ptx = util.set-auto-domain(ptx, (old.axes.at(1),), (old.y-domain,)) + } + + let data = ( + axes: old.axes, + label: old.at("label", default: none), + style: old.at("style", default: none), + stroke: (ptx, transform) => { + if "plot-stroke" in old { + draw-old(ptx, transform, (old.plot-stroke)(old, make-cptx(ptx, old))) + } + }, + fill: (ptx, transform) => { + if "plot-fill" in old { + draw-old(ptx, transform, (old.plot-fill)(old, make-cptx(ptx, old))) + } + }, + ) + ptx.data.push(data) + return ptx + } + ) +} From f275017c6ec1d3ec4319d7e83e9e4d4f38754462 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 29 Nov 2024 13:48:38 +0100 Subject: [PATCH 10/21] sample: Add sampler argument to add/add-contour --- src/plot.typ | 2 +- src/plot/contour.typ | 13 ++++++++++--- src/plot/line.typ | 26 +++++++++++++++++++++----- src/plot/sample.typ | 26 ++++++++++++++++++++++++-- tests/plot/sample/sample.typ | 7 ++++--- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/plot.typ b/src/plot.typ index 7c0fcf9..5ddda28 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -9,7 +9,7 @@ #import "/src/sub-plot.typ" #import "/src/compat.typ" -#import "/src/plot/sample.typ": sample-fn, sample-fn2 +#import "/src/plot/sample.typ": sample, sample-int, sample-binary #import "/src/plot/line.typ": add, add-hline, add-vline, add-fill-between #import "/src/plot/contour.typ": add-contour #import "/src/plot/boxwhisker.typ": add-boxwhisker diff --git a/src/plot/contour.typ b/src/plot/contour.typ index b03ed25..8e8adbd 100644 --- a/src/plot/contour.typ +++ b/src/plot/contour.typ @@ -287,6 +287,7 @@ y-domain: (0, 1), x-samples: 25, y-samples: 25, + sampler: auto, interpolate: true, op: auto, axes: ("x", "y"), @@ -294,11 +295,17 @@ fill: false, limit: 50, ) = { + if sampler == auto { + sampler = sample.sample-binary + .with(x-domain: x-domain, + y-domain: y-domain, + x-samples: x-samples, + y-samples: y-samples) + } + // Sample a x/y function if type(data) == function { - data = sample.sample-fn2(data, - x-domain, y-domain, - x-samples, y-samples) + data = sampler(data) } // Find matrix dimensions diff --git a/src/plot/line.typ b/src/plot/line.typ index 8fcc440..b47f244 100644 --- a/src/plot/line.typ +++ b/src/plot/line.typ @@ -100,9 +100,9 @@ /// / `"shape"`: Fill the complete shape /// - samples (int): Number of times the `data` function gets called for /// sampling y-values. Only used if `data` is of type function. This parameter gets -/// passed onto `sample-fn`. +/// passed onto `sampler`. /// - sample-at (array): Array of x-values the function gets sampled at in addition -/// to the default sampling. This parameter gets passed to `sample-fn`. +/// to the default sampling. This parameter gets passed to `sampler`. /// - line (string, dictionary): Line type to use. The following types are /// supported: /// / `"raw"`: Plot raw data @@ -165,14 +165,22 @@ mark-style: (:), samples: 50, sample-at: (), + sampler: auto, line: "raw", axes: ("x", "y"), label: none, data ) = { + if sampler == auto { + sampler = sample.sample + .with(domain: domain, + samples: samples, + sample-at: sample-at) + } + // If data is of type function, sample it if type(data) == function { - data = sample.sample-fn(data, domain, samples, sample-at: sample-at) + data = sampler(data) } // Transform data @@ -392,16 +400,24 @@ domain: auto, samples: 50, sample-at: (), + sampler: auto, line: "raw", axes: ("x", "y"), label: none, style: (:)) = { + if sampler == auto { + sampler = sample.sample + .with(domain: domain, + samples: samples, + sample-at: sample-at) + } + // If data is of type function, sample it if type(data-a) == function { - data-a = sample.sample-fn(data-a, domain, samples, sample-at: sample-at) + data-a = sampler(data-a) } if type(data-b) == function { - data-b = sample.sample-fn(data-b, domain, samples, sample-at: sample-at) + data-b = sampler(data-b) } // Transform data diff --git a/src/plot/sample.typ b/src/plot/sample.typ index 3ad881d..9bee6b6 100644 --- a/src/plot/sample.typ +++ b/src/plot/sample.typ @@ -13,7 +13,7 @@ /// to the `samples` number of samples. Values outsides the /// specified domain are legal. /// -> array: Array of (x, y) tuples -#let sample-fn(fn, domain, samples, sample-at: ()) = { +#let sample(fn, domain: (0,1), samples: 25, sample-at: ()) = { assert(samples + sample-at.len() >= 2, message: "You must at least sample 2 values") assert(type(domain) == array and domain.len() == 2, @@ -41,6 +41,28 @@ }) } +/// Sample the given single parameter function `samples` times (rounded down), +/// with $diam(domain)$ integer values. +/// And returns the sampled `y` value in an array as `(x, y)` tuples. +/// +/// If the functions first return value is a tuple `(x, y)`, then all return values +/// must be a tuple. +/// +/// - fn (function): Function to sample of the form `(x) => y` or `(t) => (x, y)`, where +/// `x` or `t` are `float` values within the domain specified by `domain`. +/// - domain (domain): Domain of `fn` used as bounding interval for the sampling points. +/// - sample-at (array): List of x values the function gets sampled at in addition +/// to the `samples` number of samples. Values outsides the +/// specified domain are legal. +/// -> array: Array of (x, y) tuples +#let sample-int(fn, domain: (0,1), sample-at: ()) = { + let (low, high) = domain.map(calc.floor) + let samples = high - low + 1 + + return sample(n => fn(int(n)), domain: domain, samples: samples, + sample-at: sample-at.map(calc.floor)) +} + /// Samples the given two parameter function with `x-samples` and /// `y-samples` values evenly spaced within the range given by /// `x-domain` and `y-domain` and returns each sampled output in @@ -54,7 +76,7 @@ /// - x-samples (int): Number of samples in the x-domain. /// - y-samples (int): Number of samples in the y-domain. /// -> array: Array of z scalars -#let sample-fn2(fn, x-domain, y-domain, x-samples, y-samples) = { +#let sample-binary(fn, x-domain: (0,1), y-domain: (0,1), x-samples: 25, y-samples: 25) = { assert(x-samples >= 2, message: "You must at least sample 2 x-values") assert(y-samples >= 2, diff --git a/tests/plot/sample/sample.typ b/tests/plot/sample/sample.typ index 54d617e..132c82b 100644 --- a/tests/plot/sample/sample.typ +++ b/tests/plot/sample/sample.typ @@ -7,7 +7,7 @@ (samples: 2, res: ((0,0), (50,5.0), (60,6.0), (100,10)), extra: (50,60)), ) #for c in cases { - let pts = plot.sample-fn(x => x/10, (0, 100), c.samples, + let pts = plot.sample(x => x/10, (0, 100), samples: c.samples, sample-at: c.at("extra", default: ())) assert.eq(pts, c.res, message: "Expected: " + repr(c.res) + ", got: " + repr(pts)) @@ -21,8 +21,9 @@ (100,150,200))), ) #for c in cases { - let rows = plot.sample-fn2((x, y) => x + y, (0, 100), (0,100), - c.samples.at(0), c.samples.at(1)) + let rows = plot.sample-binary((x, y) => x + y, x-domain: (0, 100), y-domain: (0,100), + x-samples: c.samples.at(0), + y-samples: c.samples.at(1)) assert.eq(rows, c.res, message: "Expected: " + repr(c.res) + ", got: " + repr(rows)) } From 18baf6c00e0617a9e45a8be6a4c7eb398e0444ff Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 29 Nov 2024 14:10:04 +0100 Subject: [PATCH 11/21] compat: Fix some axis & styling issues --- src/compat.typ | 14 +++++++++++--- src/plot.typ | 13 +++++++++++-- tests/axes/log-mode/test.typ | 7 ++++--- tests/plot/comb/test.typ | 9 +++++---- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/compat.typ b/src/compat.typ index 84351db..3dd0a76 100644 --- a/src/compat.typ +++ b/src/compat.typ @@ -4,6 +4,14 @@ #let make-cptx(ptx, old) = { let axes = old.axes.map(name => ptx.axes.at(name)) + axes = axes.map(ax => { + if ax.min == none or ax.max == none { + // Compat elements need the axis domain very early + ax.min = ax.auto-domain.at(0) + ax.max = ax.auto-domain.at(1) + } + return ax + }) return ( axes: axes, ) @@ -35,15 +43,15 @@ priority: 0, fn: ptx => { let old = old - if "plot-prepare" in old { - old = (old.plot-prepare)(old, make-cptx(ptx, old)) - } if "x-domain" in old { ptx = util.set-auto-domain(ptx, (old.axes.at(0),), (old.x-domain,)) } if "y-domain" in old { ptx = util.set-auto-domain(ptx, (old.axes.at(1),), (old.y-domain,)) } + if "plot-prepare" in old { + old = (old.plot-prepare)(old, make-cptx(ptx, old)) + } let data = ( axes: old.axes, diff --git a/src/plot.typ b/src/plot.typ index 5ddda28..7e53618 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -299,12 +299,21 @@ let style = if type(plot-style) == function { (plot-style)(i) } else if type(plot-style) == array { - plot-style.at(i) + plot-style.at(calc.rem(i, plot-style.len())) } else { plot-style } - data.style = cetz.util.merge-dictionary(style, data.at("style", default: (:))) + let data-style = data.at("style", default: (:)) + if type(data-style) == function { + data-style = (data-style)(i) + } + + data.style = if style != none { + cetz.util.merge-dictionary(style, data-style) + } else { + data-style + } return data }) diff --git a/tests/axes/log-mode/test.typ b/tests/axes/log-mode/test.typ index 37bcdaf..8147b0d 100644 --- a/tests/axes/log-mode/test.typ +++ b/tests/axes/log-mode/test.typ @@ -62,13 +62,13 @@ plot.plot( size: (16, 6), 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.log-axis("x") plot.add(domain: (0.01, 100), x => {0}) } ) @@ -81,7 +81,6 @@ ) plot.plot( size: (16, 6), - 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)$)], @@ -89,12 +88,14 @@ x-grid: "both", y-grid: "both", { + plot.log-axis("x") plot.add(domain: (0.01, 100), x => {-40}) } ) }) }) +/* // Scatter plot test #box(stroke: 2pt + red, canvas({ @@ -102,13 +103,13 @@ plot.plot( size: (9, 6), - 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.log-axis("y", base: 100) plot.add( ((0, 1),(1,2),(1,3),(2, 100),(2,150),(3, 1000),), style: (stroke: none), diff --git a/tests/plot/comb/test.typ b/tests/plot/comb/test.typ index 36dc317..e3d5f6c 100644 --- a/tests/plot/comb/test.typ +++ b/tests/plot/comb/test.typ @@ -141,15 +141,15 @@ Uniform marks across the series ) }) -= Logarithym // Test pending upstream #test-case({ plot.plot( size: (10,6), // x-min: 35, x-max: 45, y-max: 100, - y-mode: "log", y-tick-step: 1, y-base: 10, y-format: "sci", y-minor-tick-step: 1, + y-tick-step: 1, y-base: 10, y-format: "sci", y-minor-tick-step: 1, { + plot.log-axis("y") plot.add-comb( label: "Linalool, 70eV", mark: "o", @@ -166,8 +166,9 @@ Uniform marks across the series size: (10,6), x-min: 10, x-max: 1000, y-max: 100, y-tick-step: 20, - x-mode: "log", x-tick-step: 1, x-base: 10, x-format: "sci", + x-tick-step: 1, x-base: 10, x-format: "sci", { + plot.log-axis("x") plot.add-comb( label: "Linalool, 70eV", mark: "x", @@ -176,4 +177,4 @@ Uniform marks across the series ) } ) -}) \ No newline at end of file +}) From 2ac6151bcf101b86b64e635d002df351f4c68b48 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 29 Nov 2024 14:24:56 +0100 Subject: [PATCH 12/21] spine: Move scientific to its own file --- src/spine.typ | 252 ++------------------------------------- src/spine/scientific.typ | 86 +++++++++++++ src/spine/util.typ | 12 ++ src/style.typ | 149 +++++++++++++++++++++++ 4 files changed, 257 insertions(+), 242 deletions(-) create mode 100644 src/spine/scientific.typ create mode 100644 src/spine/util.typ create mode 100644 src/style.typ diff --git a/src/spine.typ b/src/spine.typ index 15e7ce6..71f221f 100644 --- a/src/spine.typ +++ b/src/spine.typ @@ -4,115 +4,9 @@ #import "/src/ticks.typ" #import "/src/projection.typ" #import "/src/axis.typ" - -/// Default axis style -/// -/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.]) -/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.]) -/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.]) -/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.]) -/// -/// #show-parameter-block("stroke", "stroke", [Axis stroke style.]) -/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.]) -/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.]) -/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.]) -/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see @@on-layer() )]) -/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see @@on-layer() )]) -/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see @@on-layer() )]) -/// #show-parameter-block("padding", "number", [Extra distance between axes and plotting area. For schoolbook axes, this is the length of how much axes grow out of the plotting area.]) -/// #show-parameter-block("tick.stroke", "stroke", [Major tick stroke style.]) -/// #show-parameter-block("tick.minor-stroke", "stroke", [Minor tick stroke style.]) -/// #show-parameter-block("tick.offset", ("number", "ratio"), [Major tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.minor-offset", ("number", "ratio"), [Minor tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.length", ("number"), [Major tick length.]) -/// #show-parameter-block("tick.minor-length", ("number", "ratio"), [Minor tick length, can be relative to the major tick length.]) -/// #show-parameter-block("tick.label.offset", ("number"), [Major tick label offset away from the tick.]) -/// #show-parameter-block("tick.label.angle", ("angle"), [Major tick label angle.]) -/// #show-parameter-block("tick.label.anchor", ("anchor"), [Anchor of major tick labels used for positioning.]) -/// #show-parameter-block("tick.label.show", ("auto", "bool"), default: auto, [Set visibility of tick labels. A value of `auto` shows tick labels for all but mirrored axes.]) -/// #show-parameter-block("grid.stroke", "stroke", [Major grid line stroke style.]) -/// #show-parameter-block("grid.minor-stroke", "stroke", [Minor grid line stroke style.]) -/// #show-parameter-block("break-point.width", "number", [Axis break width along the axis.]) -/// #show-parameter-block("break-point.length", "number", [Axis break length.]) -/// -/// #show-parameter-block("shared-zero", ("bool", "content"), default: "$0$", [School-book style axes only: Content to display at the plots origin (0,0). If set to `false`, nothing is shown. Having this set, suppresses auto-generated ticks for $0$!]) -#let default-style = ( - mark: none, - stroke: (paint: black, cap: "square"), - fill: none, - - padding: (0cm, 0cm), - - show-zero: true, - zero-label: $0$, - - axis-layer: 0, - tick-layer: 0, - grid-layer: 0, - - tick: ( - stroke: black + 1pt, - minor-stroke: black + .5pt, - - offset: 0cm, - length: .2cm, - minor-offset: 0cm, - minor-length: .1cm, - flip: false, - - label: ( - "show": auto, - offset: .1cm, - angle: 0deg, - anchor: "center", - draw: (pos, body, angle, anchor) => { - draw.content(pos, body, angle: angle, anchor: anchor) - }, - ), - ), - - grid: ( - stroke: black + .5pt, - minor-stroke: black + .25pt, - ), - - // Overrides - x: ( - tick: ( - label: ( - anchor: "north", - ), - ), - ), - y: ( - tick: ( - label: ( - anchor: "east", - ), - ), - ), - u: ( - tick: ( - label: ( - anchor: "south", - ), - ), - ), - v: ( - tick: ( - label: ( - anchor: "west", - ), - ), - ), - distal: ( - tick: ( - label: ( - anchor: "east", - ) - ) - ), -) +#import "/src/style.typ": prepare-style, get-axis-style, default-style +#import "/src/spine/scientific.typ": cartesian-scientific +#import "/src/spine/util.typ": cartesian-axis-projection /// Default schoolbook style #let default-style-schoolbook = cetz.util.merge-dictionary(default-style, ( @@ -127,132 +21,6 @@ ), )) -#let _prepare-style(ptx, style) = { - let ctx = ptx.cetz-ctx - let resolve-number = cetz.util.resolve-number.with(ctx) - let relative-to(val, to) = { - return if type(val) == ratio { - val * to - } else { - val - } - } - let resolve-relative-number(val, to) = { - return resolve-number(relative-to(val, to)) - } - - if type(style.padding) != array { - style.padding = (style.padding,) * 2 - } - style.padding = style.padding.map(resolve-number) - - style.tick.offset = resolve-number(style.tick.offset) - style.tick.length = resolve-number(style.tick.length) - style.tick.minor-offset = resolve-relative-number(style.tick.minor-offset, style.tick.offset) - style.tick.minor-length = resolve-relative-number(style.tick.minor-length, style.tick.length) - - style.tick.label.offset = resolve-number(style.tick.label.offset) - - return style -} - -#let _get-axis-style(ptx, style, name) = { - return _prepare-style(ptx, if name in style { - cetz.util.merge-dictionary(style, style.at(name, default: (:))) - } else { - style - }) -} - -/// -#let cartesian-axis-projection(ax, start, stop) = { - let dir = vector.norm(vector.sub(stop, start)) - let dist = vector.dist(start, stop) - return (value) => { - vector.add(start, vector.scale(dir, axis.transform(ax, value, 0, dist))) - } -} - - -/// -#let cartesian-scientific(projections: none, name: none, style: (:)) = { - return ( - name: name, - draw: (ptx) => { - let xy-proj = projections.at(0) - let uv-proj = projections.at(1, default: xy-proj) - let has-uv = projections.len() > 1 - let (x, y) = xy-proj.axes - let (u, v) = uv-proj.axes - - let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, - root: "axes", merge: style, base: default-style)) - let x-style = _get-axis-style(ptx, style, "x") - let y-style = _get-axis-style(ptx, style, "y") - let u-style = _get-axis-style(ptx, style, "u") - let v-style = _get-axis-style(ptx, style, "v") - - let (x-low, x-high, y-low, y-high) = (xy-proj.transform)( - (x.min, y.min), (x.max, y.min), - (x.min, y.min), (x.min, y.max), - ) - let (u-low, u-high, v-low, v-high) = (uv-proj.transform)( - (u.min, v.max), (u.max, v.max), - (u.max, v.min), (u.max, v.max), - ) - - let move-vec(v, direction, length) = { - vector.add(v, direction.enumerate().map(((i, v)) => v * length.at(i))) - } - - // Outset axes - x-low = move-vec(x-low, (0, -1), x-style.padding) - x-high = move-vec(x-high, (0, -1), x-style.padding) - y-low = move-vec(y-low, (-1, 0), y-style.padding) - y-high = move-vec(y-high, (-1, 0), y-style.padding) - u-low = move-vec(u-low, (0, 1), u-style.padding) - u-high = move-vec(u-high, (0, 1), u-style.padding) - v-low = move-vec(v-low, (1, 0), v-style.padding) - v-high = move-vec(v-high, (1, 0), v-style.padding) - - // Frame corners (FIX for uv axes) - let south-west = move-vec(x-low, (-1, 0), x-style.padding) - let south-east = move-vec(x-high, (+1, 0), x-style.padding) - let north-west = move-vec(u-low, (-1, 0), u-style.padding) - let north-east = move-vec(u-high, (+1, 0), u-style.padding) - - // Grid lengths - let x-grid-length = u-low.at(1) - x-low.at(1) - let y-grid-length = v-low.at(0) - y-low.at(0) - let u-grid-length = x-low.at(1) - u-low.at(1) - let v-grid-length = y-low.at(0) - v-low.at(0) - - let axes = ( - (x, (0,+1), (0,x-grid-length), cartesian-axis-projection(x, x-low, x-high), x-style, false), - (y, (+1,0), (y-grid-length,0), cartesian-axis-projection(y, y-low, y-high), y-style, false), - (u, (0,-1), (0,u-grid-length), cartesian-axis-projection(u, u-low, u-high), u-style, not has-uv), - (v, (-1,0), (v-grid-length,0), cartesian-axis-projection(v, v-low, v-high), v-style, not has-uv), - ) - - draw.group(name: "spine", { - for (ax, dir, grid-dir, proj, style, mirror) in axes { - draw.on-layer(style.axis-layer, { - draw.line(proj(ax.min), proj(ax.max), stroke: style.stroke, mark: style.mark) - }) - if "computed-ticks" in ax { - if not mirror { - ticks.draw-cartesian-grid(proj, grid-dir, ax, ax.computed-ticks, style) - } - ticks.draw-cartesian(proj, dir, ax.computed-ticks, style, is-mirror: mirror) - } - } - }) - - // TODO: Draw labels - }, - ) -} - /// #let schoolbook(projections: none, name: none, zero: (0, 0), ..style) = { return ( @@ -264,10 +32,10 @@ let y = axes.at(1) let z = axes.at(2, default: none) - let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, root: "axes", merge: style.named(), base: default-style-schoolbook)) - let x-style = _get-axis-style(ptx, style, "x") - let y-style = _get-axis-style(ptx, style, "y") + let x-style = get-axis-style(ptx, style, "x") + let y-style = get-axis-style(ptx, style, "y") let zero-x = calc.max(x.min, calc.min(0, x.max)) let zero-y = calc.max(y.min, calc.min(0, y.max)) @@ -352,10 +120,10 @@ let radius = vector.dist(origin, start) - let style = _prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, - root: "axes", merge: style.named(), base: default-style)) - let angular-style = _get-axis-style(ptx, style, "angular") - let distal-style = _get-axis-style(ptx, style, "distal") + let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + root: "axes", merge: style.named(), base: style.default-style)) + let angular-style = get-axis-style(ptx, style, "angular") + let distal-style = get-axis-style(ptx, style, "distal") let r-padding = angular-style.padding.first() let r-start = origin diff --git a/src/spine/scientific.typ b/src/spine/scientific.typ new file mode 100644 index 0000000..10ee49a --- /dev/null +++ b/src/spine/scientific.typ @@ -0,0 +1,86 @@ +#import "/src/cetz.typ" +#import "/src/axis.typ" +#import "/src/ticks.typ" +#import "/src/style.typ": prepare-style, get-axis-style, default-style +#import "/src/spine/util.typ": cartesian-axis-projection + +#import cetz: vector, draw + +/// +#let cartesian-scientific(projections: none, name: none, style: (:)) = { + return ( + name: name, + draw: (ptx) => { + let xy-proj = projections.at(0) + let uv-proj = projections.at(1, default: xy-proj) + let has-uv = projections.len() > 1 + let (x, y) = xy-proj.axes + let (u, v) = uv-proj.axes + + let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + root: "axes", merge: style, base: default-style)) + let x-style = get-axis-style(ptx, style, "x") + let y-style = get-axis-style(ptx, style, "y") + let u-style = get-axis-style(ptx, style, "u") + let v-style = get-axis-style(ptx, style, "v") + + let (x-low, x-high, y-low, y-high) = (xy-proj.transform)( + (x.min, y.min), (x.max, y.min), + (x.min, y.min), (x.min, y.max), + ) + let (u-low, u-high, v-low, v-high) = (uv-proj.transform)( + (u.min, v.max), (u.max, v.max), + (u.max, v.min), (u.max, v.max), + ) + + let move-vec(v, direction, length) = { + vector.add(v, direction.enumerate().map(((i, v)) => v * length.at(i))) + } + + // Outset axes + x-low = move-vec(x-low, (0, -1), x-style.padding) + x-high = move-vec(x-high, (0, -1), x-style.padding) + y-low = move-vec(y-low, (-1, 0), y-style.padding) + y-high = move-vec(y-high, (-1, 0), y-style.padding) + u-low = move-vec(u-low, (0, 1), u-style.padding) + u-high = move-vec(u-high, (0, 1), u-style.padding) + v-low = move-vec(v-low, (1, 0), v-style.padding) + v-high = move-vec(v-high, (1, 0), v-style.padding) + + // Frame corners (FIX for uv axes) + let south-west = move-vec(x-low, (-1, 0), x-style.padding) + let south-east = move-vec(x-high, (+1, 0), x-style.padding) + let north-west = move-vec(u-low, (-1, 0), u-style.padding) + let north-east = move-vec(u-high, (+1, 0), u-style.padding) + + // Grid lengths + let x-grid-length = u-low.at(1) - x-low.at(1) + let y-grid-length = v-low.at(0) - y-low.at(0) + let u-grid-length = x-low.at(1) - u-low.at(1) + let v-grid-length = y-low.at(0) - v-low.at(0) + + let axes = ( + (x, (0,+1), (0,x-grid-length), cartesian-axis-projection(x, x-low, x-high), x-style, false), + (y, (+1,0), (y-grid-length,0), cartesian-axis-projection(y, y-low, y-high), y-style, false), + (u, (0,-1), (0,u-grid-length), cartesian-axis-projection(u, u-low, u-high), u-style, not has-uv), + (v, (-1,0), (v-grid-length,0), cartesian-axis-projection(v, v-low, v-high), v-style, not has-uv), + ) + + draw.group(name: "spine", { + for (ax, dir, grid-dir, proj, style, mirror) in axes { + draw.on-layer(style.axis-layer, { + draw.line(proj(ax.min), proj(ax.max), stroke: style.stroke, mark: style.mark) + }) + if "computed-ticks" in ax { + if not mirror { + ticks.draw-cartesian-grid(proj, grid-dir, ax, ax.computed-ticks, style) + } + ticks.draw-cartesian(proj, dir, ax.computed-ticks, style, is-mirror: mirror) + } + } + }) + + // TODO: Draw labels + }, + ) +} diff --git a/src/spine/util.typ b/src/spine/util.typ new file mode 100644 index 0000000..79e49cf --- /dev/null +++ b/src/spine/util.typ @@ -0,0 +1,12 @@ +#import "/src/cetz.typ": vector +#import "/src/axis.typ" + +/// Returns a function that interpolates from an axis value +/// between start and stop +#let cartesian-axis-projection(ax, start, stop) = { + let dir = vector.norm(vector.sub(stop, start)) + let dist = vector.dist(start, stop) + return (value) => { + vector.add(start, vector.scale(dir, axis.transform(ax, value, 0, dist))) + } +} diff --git a/src/style.typ b/src/style.typ new file mode 100644 index 0000000..c25ad5d --- /dev/null +++ b/src/style.typ @@ -0,0 +1,149 @@ +#import "/src/cetz.typ" +#import cetz: draw + +/// Default axis style +/// +/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.]) +/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.]) +/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.]) +/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.]) +/// +/// #show-parameter-block("stroke", "stroke", [Axis stroke style.]) +/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.]) +/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.]) +/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.]) +/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see @@on-layer() )]) +/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see @@on-layer() )]) +/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see @@on-layer() )]) +/// #show-parameter-block("padding", "number", [Extra distance between axes and plotting area. For schoolbook axes, this is the length of how much axes grow out of the plotting area.]) +/// #show-parameter-block("tick.stroke", "stroke", [Major tick stroke style.]) +/// #show-parameter-block("tick.minor-stroke", "stroke", [Minor tick stroke style.]) +/// #show-parameter-block("tick.offset", ("number", "ratio"), [Major tick offset along the tick's direction, can be relative to the length.]) +/// #show-parameter-block("tick.minor-offset", ("number", "ratio"), [Minor tick offset along the tick's direction, can be relative to the length.]) +/// #show-parameter-block("tick.length", ("number"), [Major tick length.]) +/// #show-parameter-block("tick.minor-length", ("number", "ratio"), [Minor tick length, can be relative to the major tick length.]) +/// #show-parameter-block("tick.label.offset", ("number"), [Major tick label offset away from the tick.]) +/// #show-parameter-block("tick.label.angle", ("angle"), [Major tick label angle.]) +/// #show-parameter-block("tick.label.anchor", ("anchor"), [Anchor of major tick labels used for positioning.]) +/// #show-parameter-block("tick.label.show", ("auto", "bool"), default: auto, [Set visibility of tick labels. A value of `auto` shows tick labels for all but mirrored axes.]) +/// #show-parameter-block("grid.stroke", "stroke", [Major grid line stroke style.]) +/// #show-parameter-block("grid.minor-stroke", "stroke", [Minor grid line stroke style.]) +/// #show-parameter-block("break-point.width", "number", [Axis break width along the axis.]) +/// #show-parameter-block("break-point.length", "number", [Axis break length.]) +/// +/// #show-parameter-block("shared-zero", ("bool", "content"), default: "$0$", [School-book style axes only: Content to display at the plots origin (0,0). If set to `false`, nothing is shown. Having this set, suppresses auto-generated ticks for $0$!]) +#let default-style = ( + mark: none, + stroke: (paint: black, cap: "square"), + fill: none, + + padding: (0cm, 0cm), + + show-zero: true, + zero-label: $0$, + + axis-layer: 0, + tick-layer: 0, + grid-layer: 0, + + tick: ( + stroke: black + 1pt, + minor-stroke: black + .5pt, + + offset: 0cm, + length: .2cm, + minor-offset: 0cm, + minor-length: .1cm, + flip: false, + + label: ( + "show": auto, + offset: .1cm, + angle: 0deg, + anchor: "center", + draw: (pos, body, angle, anchor) => { + draw.content(pos, body, angle: angle, anchor: anchor) + }, + ), + ), + + grid: ( + stroke: black + .5pt, + minor-stroke: black + .25pt, + ), + + // Overrides + x: ( + tick: ( + label: ( + anchor: "north", + ), + ), + ), + y: ( + tick: ( + label: ( + anchor: "east", + ), + ), + ), + u: ( + tick: ( + label: ( + anchor: "south", + ), + ), + ), + v: ( + tick: ( + label: ( + anchor: "west", + ), + ), + ), + distal: ( + tick: ( + label: ( + anchor: "east", + ) + ) + ), +) + +#let prepare-style(ptx, style) = { + let ctx = ptx.cetz-ctx + let resolve-number = cetz.util.resolve-number.with(ctx) + let relative-to(val, to) = { + return if type(val) == ratio { + val * to + } else { + val + } + } + let resolve-relative-number(val, to) = { + return resolve-number(relative-to(val, to)) + } + + if type(style.padding) != array { + style.padding = (style.padding,) * 2 + } + style.padding = style.padding.map(resolve-number) + + style.tick.offset = resolve-number(style.tick.offset) + style.tick.length = resolve-number(style.tick.length) + style.tick.minor-offset = resolve-relative-number(style.tick.minor-offset, style.tick.offset) + style.tick.minor-length = resolve-relative-number(style.tick.minor-length, style.tick.length) + + style.tick.label.offset = resolve-number(style.tick.label.offset) + + return style +} + +/// Get merged (sub) style for an axis +#let get-axis-style(ptx, style, name) = { + return prepare-style(ptx, if name in style { + cetz.util.merge-dictionary(style, style.at(name, default: (:))) + } else { + style + }) +} From 7c5483dafb55b79dcd2ce224c0d0c6ca834f34f3 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 29 Nov 2024 14:29:53 +0100 Subject: [PATCH 13/21] scientific: Fix axis style keys --- src/spine/scientific.typ | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spine/scientific.typ b/src/spine/scientific.typ index 10ee49a..703d581 100644 --- a/src/spine/scientific.typ +++ b/src/spine/scientific.typ @@ -19,10 +19,10 @@ let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, root: "axes", merge: style, base: default-style)) - let x-style = get-axis-style(ptx, style, "x") - let y-style = get-axis-style(ptx, style, "y") - let u-style = get-axis-style(ptx, style, "u") - let v-style = get-axis-style(ptx, style, "v") + let x-style = get-axis-style(ptx, style, x.name) + let y-style = get-axis-style(ptx, style, y.name) + let u-style = get-axis-style(ptx, style, u.name) + let v-style = get-axis-style(ptx, style, v.name) let (x-low, x-high, y-low, y-high) = (xy-proj.transform)( (x.min, y.min), (x.max, y.min), From 5e1244f87186e7654354847e1abd9b7a4ded654e Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 29 Nov 2024 14:38:10 +0100 Subject: [PATCH 14/21] spine: Split spines to separate files --- src/spine.typ | 162 +-------------------------------------- src/spine/polar.typ | 68 ++++++++++++++++ src/spine/schoolbook.typ | 97 +++++++++++++++++++++++ src/spine/scientific.typ | 2 +- src/sub-plot.typ | 8 +- 5 files changed, 176 insertions(+), 161 deletions(-) create mode 100644 src/spine/polar.typ create mode 100644 src/spine/schoolbook.typ diff --git a/src/spine.typ b/src/spine.typ index 71f221f..53ac482 100644 --- a/src/spine.typ +++ b/src/spine.typ @@ -1,159 +1,3 @@ -#import "/src/cetz.typ" -#import cetz: vector, draw - -#import "/src/ticks.typ" -#import "/src/projection.typ" -#import "/src/axis.typ" -#import "/src/style.typ": prepare-style, get-axis-style, default-style -#import "/src/spine/scientific.typ": cartesian-scientific -#import "/src/spine/util.typ": cartesian-axis-projection - -/// Default schoolbook style -#let default-style-schoolbook = cetz.util.merge-dictionary(default-style, ( - mark: (end: "straight"), - padding: (.1cm, 1em), - - tick: ( - offset: -.1cm, - length: .2cm, - minor-offset: -.05cm, - minor-length: .1cm, - ), -)) - -/// -#let schoolbook(projections: none, name: none, zero: (0, 0), ..style) = { - return ( - name: name, - draw: (ptx) => { - let proj = projections.at(0) - let axes = proj.axes - let x = axes.at(0) - let y = axes.at(1) - let z = axes.at(2, default: none) - - let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, - root: "axes", merge: style.named(), base: default-style-schoolbook)) - let x-style = get-axis-style(ptx, style, "x") - let y-style = get-axis-style(ptx, style, "y") - - let zero-x = calc.max(x.min, calc.min(0, x.max)) - let zero-y = calc.max(y.min, calc.min(0, y.max)) - let zero-pt = ( - calc.max(x.min, calc.min(zero.at(0), x.max)), - calc.max(y.min, calc.min(zero.at(1), y.max)), - ) - - let (zero, min-x, max-x, min-y, max-y) = (proj.transform)( - zero-pt, - vector.add(zero-pt, (x.min, zero-y)), vector.add(zero-pt, (x.max, zero-y)), - vector.add(zero-pt, (zero-x, y.min)), vector.add(zero-pt, (zero-x, y.max)), - ) - - let x-padding = x-style.padding - if type(x-padding) != array { - x-padding = (x-padding, x-padding) - } - - let y-padding = y-style.padding - if type(y-padding) != array { - y-padding = (y-padding, y-padding) - } - - let outset-lo-x = (x-padding.at(0), 0) - let outset-hi-x = (x-padding.at(1), 0) - let outset-lo-y = (0., y-padding.at(0)) - let outset-hi-y = (0., y-padding.at(1)) - let outset-min-x = vector.scale(outset-lo-x, -1) - let outset-max-x = vector.scale(outset-hi-x, +1) - let outset-min-y = vector.scale(outset-lo-y, -1) - let outset-max-y = vector.scale(outset-hi-y, +1) - - - draw.on-layer(x-style.axis-layer, { - draw.line((rel: outset-min-x, to: min-x), - (rel: outset-max-x, to: max-x), - mark: x-style.mark, - stroke: x-style.stroke) - }) - if "computed-ticks" in x { - //ticks.draw-cartesian-grid(grid-proj, grid-dir, ax, ax.computed-ticks, style) - let tick-proj = cartesian-axis-projection(x, min-x, max-x) - ticks.draw-cartesian(tick-proj, (0,+1), x.computed-ticks, x-style) - } - - draw.on-layer(y-style.axis-layer, { - draw.line((rel: outset-min-y, to: min-y), - (rel: outset-max-y, to: max-y), - mark: y-style.mark, - stroke: y-style.stroke) - }) - if "computed-ticks" in y { - //ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) - let tick-proj = cartesian-axis-projection(y, min-y, max-y) - ticks.draw-cartesian(tick-proj, (+1,0), y.computed-ticks, y-style) - } - } - ) -} - -/// Polar frame -#let polar(projections: none, name: none, ..style) = { - assert(projections.len() == 1, - message: "Unexpected number of projections!") - - return ( - name: name, - draw: (ptx) => { - let proj = projections.first() - let angular = proj.axes.at(0) - let distal = proj.axes.at(1) - - let (origin, start, mid, stop) = (proj.transform)( - (angular.min, distal.min), - (angular.min, distal.max), - ((angular.min + angular.max) / 2, distal.max), - (angular.max, distal.max), - ) - start = start.map(calc.round.with(digits: 6)) - stop = stop.map(calc.round.with(digits: 6)) - - let radius = vector.dist(origin, start) - - let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, - root: "axes", merge: style.named(), base: style.default-style)) - let angular-style = get-axis-style(ptx, style, "angular") - let distal-style = get-axis-style(ptx, style, "distal") - - let r-padding = angular-style.padding.first() - let r-start = origin - let r-end = vector.add(origin, (0, radius)) - draw.line(r-start, (rel: (0, radius + r-padding)), stroke: distal-style.stroke) - if "computed-ticks" in distal { - // TODO - ticks.draw-distal-grid(proj, distal.computed-ticks, distal-style) - //ticks.draw-cartesian(r-start, r-end, distal.computed-ticks, distal-style) - } - - if start == stop { - draw.circle(origin, radius: radius + r-padding, - stroke: angular-style.stroke, - fill: angular-style.fill) - } else { - // Apply padding to all three points - (start, mid, stop) = (start, mid, stop).map(pt => { - vector.add(pt, vector.scale(vector.norm(vector.sub(pt, origin)), r-padding)) - }) - - draw.arc-through(start, mid, stop, - stroke: angular-style.stroke, - fill: angular-style.fill, - mode: "PIE") - } - if "computed-ticks" in angular { - ticks.draw-angular-grid(proj, angular.computed-ticks, angular-style) - // TODO - } - }, - ) -} +#import "/src/spine/scientific.typ": scientific +#import "/src/spine/schoolbook.typ": schoolbook +#import "/src/spine/polar.typ": polar diff --git a/src/spine/polar.typ b/src/spine/polar.typ new file mode 100644 index 0000000..e7bc451 --- /dev/null +++ b/src/spine/polar.typ @@ -0,0 +1,68 @@ +#import "/src/cetz.typ" +#import "/src/axis.typ" +#import "/src/ticks.typ" +#import "/src/style.typ": prepare-style, get-axis-style, default-style +#import "/src/spine/util.typ": cartesian-axis-projection + +#import cetz: vector, draw + +/// Polar frame +#let polar(projections: none, name: none, ..style) = { + assert(projections.len() == 1, + message: "Unexpected number of projections!") + + return ( + name: name, + draw: (ptx) => { + let proj = projections.first() + let angular = proj.axes.at(0) + let distal = proj.axes.at(1) + + let (origin, start, mid, stop) = (proj.transform)( + (angular.min, distal.min), + (angular.min, distal.max), + ((angular.min + angular.max) / 2, distal.max), + (angular.max, distal.max), + ) + start = start.map(calc.round.with(digits: 6)) + stop = stop.map(calc.round.with(digits: 6)) + + let radius = vector.dist(origin, start) + + let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + root: "axes", merge: style.named(), base: style.default-style)) + let angular-style = get-axis-style(ptx, style, "angular") + let distal-style = get-axis-style(ptx, style, "distal") + + let r-padding = angular-style.padding.first() + let r-start = origin + let r-end = vector.add(origin, (0, radius)) + draw.line(r-start, (rel: (0, radius + r-padding)), stroke: distal-style.stroke) + if "computed-ticks" in distal { + // TODO + ticks.draw-distal-grid(proj, distal.computed-ticks, distal-style) + //ticks.draw-cartesian(r-start, r-end, distal.computed-ticks, distal-style) + } + + if start == stop { + draw.circle(origin, radius: radius + r-padding, + stroke: angular-style.stroke, + fill: angular-style.fill) + } else { + // Apply padding to all three points + (start, mid, stop) = (start, mid, stop).map(pt => { + vector.add(pt, vector.scale(vector.norm(vector.sub(pt, origin)), r-padding)) + }) + + draw.arc-through(start, mid, stop, + stroke: angular-style.stroke, + fill: angular-style.fill, + mode: "PIE") + } + if "computed-ticks" in angular { + ticks.draw-angular-grid(proj, angular.computed-ticks, angular-style) + // TODO + } + }, + ) +} diff --git a/src/spine/schoolbook.typ b/src/spine/schoolbook.typ new file mode 100644 index 0000000..9af43e0 --- /dev/null +++ b/src/spine/schoolbook.typ @@ -0,0 +1,97 @@ +#import "/src/cetz.typ" +#import "/src/axis.typ" +#import "/src/ticks.typ" +#import "/src/style.typ": prepare-style, get-axis-style, default-style +#import "/src/spine/util.typ": cartesian-axis-projection + +#import cetz: vector, draw + +/// Default schoolbook style +#let default-style-schoolbook = cetz.util.merge-dictionary(default-style, ( + mark: (end: "straight"), + padding: (.1cm, 1em), + + tick: ( + offset: -.1cm, + length: .2cm, + minor-offset: -.05cm, + minor-length: .1cm, + ), +)) + + +/// +#let schoolbook(projections: none, name: none, zero: (0, 0), ..style) = { + return ( + name: name, + draw: (ptx) => { + let proj = projections.at(0) + let axes = proj.axes + let x = axes.at(0) + let y = axes.at(1) + let z = axes.at(2, default: none) + + let style = prepare-style(ptx, cetz.styles.resolve(ptx.cetz-ctx.style, + root: "axes", merge: style.named(), base: default-style-schoolbook)) + let x-style = get-axis-style(ptx, style, "x") + let y-style = get-axis-style(ptx, style, "y") + + let zero-x = calc.max(x.min, calc.min(0, x.max)) + let zero-y = calc.max(y.min, calc.min(0, y.max)) + let zero-pt = ( + calc.max(x.min, calc.min(zero.at(0), x.max)), + calc.max(y.min, calc.min(zero.at(1), y.max)), + ) + + let (zero, min-x, max-x, min-y, max-y) = (proj.transform)( + zero-pt, + vector.add(zero-pt, (x.min, zero-y)), vector.add(zero-pt, (x.max, zero-y)), + vector.add(zero-pt, (zero-x, y.min)), vector.add(zero-pt, (zero-x, y.max)), + ) + + let x-padding = x-style.padding + if type(x-padding) != array { + x-padding = (x-padding, x-padding) + } + + let y-padding = y-style.padding + if type(y-padding) != array { + y-padding = (y-padding, y-padding) + } + + let outset-lo-x = (x-padding.at(0), 0) + let outset-hi-x = (x-padding.at(1), 0) + let outset-lo-y = (0., y-padding.at(0)) + let outset-hi-y = (0., y-padding.at(1)) + let outset-min-x = vector.scale(outset-lo-x, -1) + let outset-max-x = vector.scale(outset-hi-x, +1) + let outset-min-y = vector.scale(outset-lo-y, -1) + let outset-max-y = vector.scale(outset-hi-y, +1) + + + draw.on-layer(x-style.axis-layer, { + draw.line((rel: outset-min-x, to: min-x), + (rel: outset-max-x, to: max-x), + mark: x-style.mark, + stroke: x-style.stroke) + }) + if "computed-ticks" in x { + //ticks.draw-cartesian-grid(grid-proj, grid-dir, ax, ax.computed-ticks, style) + let tick-proj = cartesian-axis-projection(x, min-x, max-x) + ticks.draw-cartesian(tick-proj, (0,+1), x.computed-ticks, x-style) + } + + draw.on-layer(y-style.axis-layer, { + draw.line((rel: outset-min-y, to: min-y), + (rel: outset-max-y, to: max-y), + mark: y-style.mark, + stroke: y-style.stroke) + }) + if "computed-ticks" in y { + //ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) + let tick-proj = cartesian-axis-projection(y, min-y, max-y) + ticks.draw-cartesian(tick-proj, (+1,0), y.computed-ticks, y-style) + } + } + ) +} diff --git a/src/spine/scientific.typ b/src/spine/scientific.typ index 703d581..6d95797 100644 --- a/src/spine/scientific.typ +++ b/src/spine/scientific.typ @@ -7,7 +7,7 @@ #import cetz: vector, draw /// -#let cartesian-scientific(projections: none, name: none, style: (:)) = { +#let scientific(projections: none, name: none, style: (:)) = { return ( name: name, draw: (ptx) => { diff --git a/src/sub-plot.typ b/src/sub-plot.typ index 4eebe85..6ed2491 100644 --- a/src/sub-plot.typ +++ b/src/sub-plot.typ @@ -2,8 +2,14 @@ #import "/src/projection.typ" #import "/src/cetz.typ" +/// Create a new sub-plot /// -#let new(spine: spine.cartesian-scientific, projection: projection.cartesian, origin: (0, 0), size: auto, ..axes-style) = { +/// - spine (spine): The sub-plot spine +/// - projection (projection): The projection to use +/// - origin (vector): Origin +/// - size (auto,vector): Size or auto to use plot default size +/// - ..axes-style (axis,style): Positionaln axis names (string) and style keys +#let new(spine: spine.scientific, projection: projection.cartesian, origin: (0, 0), size: auto, ..axes-style) = { let axes = axes-style.pos() let style = axes-style.named() From 5a3e70506cdead41f25d11396b6ad3407e62fe56 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 5 Dec 2024 01:10:56 +0100 Subject: [PATCH 15/21] spine: School-Book add grid lines --- src/axis.typ | 18 ++++++++++++++++-- src/spine/schoolbook.typ | 11 +++++++---- src/spine/scientific.typ | 17 +++++++++-------- src/ticks.typ | 27 +++------------------------ 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/axis.typ b/src/axis.typ index 378b507..65b8323 100644 --- a/src/axis.typ +++ b/src/axis.typ @@ -1,6 +1,19 @@ #import "/src/ticks.typ" #import "/src/plot/util.typ" +// Grid modes +#let _get-grid-mode(mode) = { + return if mode == true or mode == "major" { + 1 + } else if mode == "minor" { + 2 + } else if mode == "both" { + 3 + } else { + 0 + } +} + /// Transform linear axis value to linear space (low, high) #let _transform-lin(ax, value, low, high) = { let range = high - low @@ -25,7 +38,7 @@ name: name, min: min, max: max, base: 10, transform: _transform-lin, auto-domain: (none, none), ticks: (step: auto, minor-step: none, format: auto, list: none), - grid: none, + grid: 0, compute-ticks: ticks.compute-ticks.with("lin"), ) + options.named() @@ -35,12 +48,13 @@ name: name, min: min, max: max, base: base, transform: _transform-log, auto-domain: (none, none), ticks: (step: auto, minor-step: none, format: auto, list: none), - grid: none, + grid: 0, compute-ticks: ticks.compute-ticks.with("log"), ) + options.named() // Prepare axis #let prepare(ptx, ax) = { + ax.grid = _get-grid-mode(ax.grid) if ax.min == none { ax.min = ax.auto-domain.at(0) } if ax.max == none { ax.max = ax.auto-domain.at(1) } if ax.min == none or ax.max == none { ax.min = -1e-6; ax.max = +1e-6 } diff --git a/src/spine/schoolbook.typ b/src/spine/schoolbook.typ index 9af43e0..9e6c24b 100644 --- a/src/spine/schoolbook.typ +++ b/src/spine/schoolbook.typ @@ -3,6 +3,7 @@ #import "/src/ticks.typ" #import "/src/style.typ": prepare-style, get-axis-style, default-style #import "/src/spine/util.typ": cartesian-axis-projection +#import "/src/spine/grid.typ" #import cetz: vector, draw @@ -19,7 +20,6 @@ ), )) - /// #let schoolbook(projections: none, name: none, zero: (0, 0), ..style) = { return ( @@ -68,7 +68,6 @@ let outset-min-y = vector.scale(outset-lo-y, -1) let outset-max-y = vector.scale(outset-hi-y, +1) - draw.on-layer(x-style.axis-layer, { draw.line((rel: outset-min-x, to: min-x), (rel: outset-max-x, to: max-x), @@ -76,8 +75,10 @@ stroke: x-style.stroke) }) if "computed-ticks" in x { - //ticks.draw-cartesian-grid(grid-proj, grid-dir, ax, ax.computed-ticks, style) + let grid-offset = min-x.at(1) - min-y.at(1) + let grid-length = max-y.at(1) - min-y.at(1) let tick-proj = cartesian-axis-projection(x, min-x, max-x) + grid.draw-cartesian(tick-proj, grid-offset, grid-length, (0,1), x.computed-ticks, style.grid, x.grid) ticks.draw-cartesian(tick-proj, (0,+1), x.computed-ticks, x-style) } @@ -88,8 +89,10 @@ stroke: y-style.stroke) }) if "computed-ticks" in y { - //ticks.draw-cartesian-grid(min-y, max-y, 1, y, y.computed-ticks, min-x, max-x, y-style) + let grid-offset = min-y.at(0) - min-x.at(0) + let grid-length = max-x.at(0) - min-x.at(0) let tick-proj = cartesian-axis-projection(y, min-y, max-y) + grid.draw-cartesian(tick-proj, grid-offset, grid-length, (1,0), y.computed-ticks, style.grid, y.grid) ticks.draw-cartesian(tick-proj, (+1,0), y.computed-ticks, y-style) } } diff --git a/src/spine/scientific.typ b/src/spine/scientific.typ index 6d95797..c17595a 100644 --- a/src/spine/scientific.typ +++ b/src/spine/scientific.typ @@ -3,6 +3,7 @@ #import "/src/ticks.typ" #import "/src/style.typ": prepare-style, get-axis-style, default-style #import "/src/spine/util.typ": cartesian-axis-projection +#import "/src/spine/grid.typ" #import cetz: vector, draw @@ -56,24 +57,24 @@ // Grid lengths let x-grid-length = u-low.at(1) - x-low.at(1) let y-grid-length = v-low.at(0) - y-low.at(0) - let u-grid-length = x-low.at(1) - u-low.at(1) - let v-grid-length = y-low.at(0) - v-low.at(0) + let u-grid-length = u-low.at(1) - x-low.at(1) + let v-grid-length = v-low.at(0) - y-low.at(0) let axes = ( - (x, (0,+1), (0,x-grid-length), cartesian-axis-projection(x, x-low, x-high), x-style, false), - (y, (+1,0), (y-grid-length,0), cartesian-axis-projection(y, y-low, y-high), y-style, false), - (u, (0,-1), (0,u-grid-length), cartesian-axis-projection(u, u-low, u-high), u-style, not has-uv), - (v, (-1,0), (v-grid-length,0), cartesian-axis-projection(v, v-low, v-high), v-style, not has-uv), + (x, (0,+1), x-grid-length, cartesian-axis-projection(x, x-low, x-high), x-style, false), + (y, (+1,0), y-grid-length, cartesian-axis-projection(y, y-low, y-high), y-style, false), + (u, (0,-1), u-grid-length, cartesian-axis-projection(u, u-low, u-high), u-style, not has-uv), + (v, (-1,0), v-grid-length, cartesian-axis-projection(v, v-low, v-high), v-style, not has-uv), ) draw.group(name: "spine", { - for (ax, dir, grid-dir, proj, style, mirror) in axes { + for (ax, dir, grid-length, proj, style, mirror) in axes { draw.on-layer(style.axis-layer, { draw.line(proj(ax.min), proj(ax.max), stroke: style.stroke, mark: style.mark) }) if "computed-ticks" in ax { if not mirror { - ticks.draw-cartesian-grid(proj, grid-dir, ax, ax.computed-ticks, style) + grid.draw-cartesian(proj, 0, grid-length, dir, ax.computed-ticks, style.grid, ax.grid) } ticks.draw-cartesian(proj, dir, ax.computed-ticks, style, is-mirror: mirror) } diff --git a/src/ticks.typ b/src/ticks.typ index e71a489..88ee9be 100644 --- a/src/ticks.typ +++ b/src/ticks.typ @@ -3,6 +3,7 @@ #import "/src/plot/formats.typ" +// TODO: Remove #let _get-grid-mode(mode) = { return if mode in (true, "major") { 1 @@ -15,8 +16,9 @@ } } +// TODO: Remove #let _draw-grid(mode, is-major) = { - return mode >= 3 or (is-major and mode == 1) or mode == 2 + return (2 + int(is-major)).bit-and(mode) } // Format a tick value @@ -312,29 +314,6 @@ }) } -// Draw grid lines for the ticks of an axis -// -#let draw-cartesian-grid(proj, offset, axis, ticks, style) = { - let kind = _get-grid-mode(axis.grid) - if kind > 0 { - draw.on-layer(style.grid-layer, { - for (value, _, major) in ticks { - let start = proj(value) - let end = vector.add(start, offset) - - // Draw a minor line - if not major and kind >= 2 { - draw.line(start, end, stroke: style.grid.minor-stroke) - } - // Draw a major line - if major and (kind == 1 or kind == 3) { - draw.line(start, end, stroke: style.grid.stroke) - } - } - }) - } -} - /// Draw angular polar grid #let draw-angular-grid(projection, ticks, style) = { let (angular, distal, ..) = projection.axes From 19def48fee9a5b106702f18255cf9e5bddc47e43 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 5 Dec 2024 01:44:43 +0100 Subject: [PATCH 16/21] scientific: Draw axis labels --- src/spine/scientific.typ | 33 ++++++++++++++++++++++++++++++++- src/style.typ | 8 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/spine/scientific.typ b/src/spine/scientific.typ index c17595a..d79e57c 100644 --- a/src/spine/scientific.typ +++ b/src/spine/scientific.typ @@ -81,7 +81,38 @@ } }) - // TODO: Draw labels + let label-config = ( + ("south", "north", 0deg), + ("west", "south", 90deg), + ("north", "south", 0deg), + ("east", "north", 90deg), + ) + for (i, (side, default-anchor, default-angle)) in label-config.enumerate() { + let (ax, dir, _, proj, style, mirror) = axes.at(i) + if ax.label != none and ax.label != [] { + let pos = proj((ax.max + ax.min) / 2) + let offset = vector.scale(dir, -style.label.offset) + let is-horizontal = calc.rem(i, 2) == 0 + pos = if is-horizontal { + (pos, "|-", (rel: offset, to: "spine." + side)) + } else { + (pos, "-|", (rel: offset, to: "spine." + side)) + } + + let angle = style.label.angle + if angle == auto { + angle = default-angle + } + + let anchor = style.label.anchor + if anchor == auto { + anchor = default-anchor + } + + draw.content(pos, + [#ax.label], anchor: anchor, angle: angle) + } + } }, ) } diff --git a/src/style.typ b/src/style.typ index c25ad5d..cc72087 100644 --- a/src/style.typ +++ b/src/style.typ @@ -72,6 +72,12 @@ minor-stroke: black + .25pt, ), + label: ( + angle: auto, + offset: .5em, + anchor: auto, + ), + // Overrides x: ( tick: ( @@ -136,6 +142,8 @@ style.tick.label.offset = resolve-number(style.tick.label.offset) + style.label.offset = resolve-number(style.label.offset) + return style } From 10bf4b4648ad0c2079f88095a4c98e58b9d82861 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 5 Dec 2024 01:46:01 +0100 Subject: [PATCH 17/21] scientific: Do not draw mirrored axis labels --- src/spine/scientific.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spine/scientific.typ b/src/spine/scientific.typ index d79e57c..2c3b560 100644 --- a/src/spine/scientific.typ +++ b/src/spine/scientific.typ @@ -89,7 +89,7 @@ ) for (i, (side, default-anchor, default-angle)) in label-config.enumerate() { let (ax, dir, _, proj, style, mirror) = axes.at(i) - if ax.label != none and ax.label != [] { + if not mirror and ax.label != none and ax.label != [] { let pos = proj((ax.max + ax.min) / 2) let offset = vector.scale(dir, -style.label.offset) let is-horizontal = calc.rem(i, 2) == 0 From 9fd222e465bdd600a8a4e0be1ca3b0928054a5db Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 5 Dec 2024 02:01:16 +0100 Subject: [PATCH 18/21] plot: Implement anchors --- src/plot.typ | 13 +++++++++++-- tests/plot/test.typ | 3 +-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/plot.typ b/src/plot.typ index 7e53618..8c57e07 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -417,8 +417,17 @@ fn: ptx => { for plot in ptx.plots { for proj in plot.projections { - if axes.all(name => proj.axes.contains(name)) { - // FIXME: Broken + let axis-names = proj.axes.map(ax => ax.name) + if axes.all(name => axis-names.contains(name)) { + let position = position.enumerate().map(((i, v)) => { + return if v == "min" { + proj.axes.at(i).min + } else if v == "max" { + proj.axes.at(i).max + } else { + v + } + }) let pt = (proj.transform)(position).first() ptx.anchors.push((name, pt)) } diff --git a/tests/plot/test.typ b/tests/plot/test.typ index a3f3d2b..3badb53 100644 --- a/tests/plot/test.typ +++ b/tests/plot/test.typ @@ -167,7 +167,7 @@ plot.add(circle-data, axes: ("x", "yt"), style: (stroke: blue)) plot.add(circle-data, axes: ("x", "yb"), style: (stroke: yellow)) }) -}),)) +}) /* Anchors */ #test-case({ @@ -218,7 +218,6 @@ }) // Test plot with anchors only -/* #test-case({ import draw: * From 3de3aae4713109576371901367396c71b4ff90e0 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 5 Dec 2024 02:03:40 +0100 Subject: [PATCH 19/21] plot: Change scientific template to two axes --- src/plot.typ | 7 +++++++ src/style.typ | 4 ++-- tests/plot/grid/ref/1.png | Bin 25547 -> 26822 bytes 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plot.typ b/src/plot.typ index 8c57e07..4fd65f9 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -62,6 +62,13 @@ #let templates = ( scientific: (ptx) => { + lin-axis("x") + lin-axis("y") + //lin-axis("u") + //lin-axis("v") + sub-plot.new("x", "y") + }, + scientific-4: (ptx) => { lin-axis("x") lin-axis("y") lin-axis("u") diff --git a/src/style.typ b/src/style.typ index cc72087..73cb525 100644 --- a/src/style.typ +++ b/src/style.typ @@ -68,8 +68,8 @@ ), grid: ( - stroke: black + .5pt, - minor-stroke: black + .25pt, + stroke: gray + .5pt, + minor-stroke: gray + .25pt, ), label: ( diff --git a/tests/plot/grid/ref/1.png b/tests/plot/grid/ref/1.png index 49e291b5324ea6b3607db2b6368bdea862c2c253..828459e42fb2d34d82bc5cd1a717622152ddad74 100644 GIT binary patch literal 26822 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 literal 25547 zcmcHhbzD}<`u~pudIQoDw{%H~lz@OVNK1D&NO!j&or-jKNlCW|2uMkH2}pPMchIxX z-us+=-k;w;KOW5EUiVsSX3d&4>zZqx*K6)zS!q#JBzzF(G*v7&vqo7{~=8 z9N2T&LO2KmWAI*FNI=nLYHRcvEZFJGbQw2>2%R9`i-P-%^#H~O8D<6!rUvFS{l7Q= z{_Wof{_`|&;NQpopZWjcGXH@lkLgz4{w%7<9Ocf0H=ck8}MTYtH z?cbY!|Mu≥@l>j(^JkU#0!;{5JPt@*yy0us-TA>Ck^~{`w}R&?M6tXDl_>))ZO- zz0%-K{3;5*ohO89dq1UzISW^3LRd0m?zJwZCW(L>@x@owuqHy=s7TtL9bT#oHZ#q&1aMGD0CNE6&GoC=~hG6!zvY;DfT{l*6Ov|&t3f!glXr{j!%Vg zSCeDxt1xpyziTui+cLlFz&UH>ycFnS`c@D3z5urHzWfwn@{wDU%*l2<AfiTuMEfdmKFPsv&d zoL{0cI&a=X_tuUuuTqCDG9JwO>Qt%H&GACa1KKjWue<~ETDz5Oz6WP^G0o8H`dP3g zCF|kNeZ;c}cWRimU~_Mp)9ul$CgzoY=YqFt_aZbMS|&gnBhhbGQt&~BLB@!*YInlQXO5;JL`Tr`RwF2b2oqDULvovmFOsAs|~ zPlj_-;}he-JHLC{2U}oM1Bw}e`(KHlb_9-}Ru=hxr))ubm~6PJ|0DhH(tls||03umWeRq?6smt~?l(my~Nu7c#@Kh1uixM!rtFOKGyQh(@Z z@ij^Ko35TNld1&@x_I|>u6x6V!}7@bmCan~;`AXm&+w_nK*PX2d05c76+wq4#Q_~$ z3Tu#xL>>SA^f!9jfg@5nX=)2Wdg6r|5Xkt4k$+q z^0(4Fi1LO_TjoQj2_rl6WF}sM{BPXZlKQxbWk@j)1KXtYD&}`XixicVuqkm-VtZDM zD-ya5KPsEEB^gnRL-HXzV;;QDb(-a>TjOd~>M@hJ;UYg=1Kac(X1~SR*J+Z!%O5pk zODa~REl;?sIEir4s6>uDcvC7SgT`g)ppKj=^Sh5;+ez3o%nq6;Y8o*o{Ly2E*A=C4 z@xud#$#81<<|ln+%(#hr1{|8SLLi6Tv3l4o*7d@mN!yt-#gMs9m2Mepd~%zwK)EPJvr4`AY+jWtcB+*E{T^(-g~!lHKrc{7grqg%ZszM|uS} zVqElwD!441qXfEJGmuj)x4od=e_CnV#1z{@nfYmjRAzkhxkBFfW`>`9(P&~H)fQ&B zNY2+<_))Xta5yIxk6Zh*qol`57>dE<4mrc?9E518s|j$DdJ&Q6*hr9NLP_LosvyG7 zXAqIBhqeu~%s3Wo8co`a&``-t`GQLIVh>(E{P65Zc_Sv=8ROE-gC(EVno~wGock-# zx6+^Q`;rQgKTI1?p^XueGekeS^sK8PGvz0!m<$6=bL-9z9b9){#)egA$Roq{pKiwHkEz$P1 z5LiQy^a&Y8co4K0cOTxwAx$O9KK;!bo{8>CA57v$DQroonVm~|aTD84UM(aY`I8pL z%n2X7FJb$>Kd3(|`?z2aJIrq!o@Jmocgt`- z=`o>vF7s?PK7qggutM8sFkOz3e=Di2jOFLA^?Bm~AK~(6Q|cgY znQ7IFN7~Jvmm5u8haMb+3{Yf7FtM;{YA(?bN91-5c zV#7!09@?ej<0N`D)0SG}dM}%&x2LL`=6B@`L!aX0L(*u%9;wATYSm~mjBg&V*onWV zpzHM;HN)w&?Gk57k(S9DUbKntGtL^!H%%-o{CQZ{_5sNw_($~d<@M73#N|tD;mhlr z@xE@u0~c<>6h+yz%t$$wf~*|x#T~u_PJzpulk2rJ8ctZhciVioyi1~%C!qJ{gCCv5eWS*D1hw&`--bJ z=+!$c+l?`(fJ*s?v4Ut=t=W1GLs=IOFhA}~sfq|hP z{0$7qNJ_diY2OV%3%iPvGBW6>sN1{1;B8x4TQkzr6A=*=lxs3jKS}CKObF@e>8Yrw zaNwjroEYG(cuB7tTU=Z$M)B#>r%ZD{KR<9d)(9t5_&EkzAf>+`8A}>18yj1RubZtc z*}rql&d!#XKMQOlo5dv|iR@g;%gbx@fy;-;Vj^BpPZs8?o*0~Y@QRz8n=6PE?%IiF$$edwF3jxkyj4$eb31fDESzF?a&nrQno?F# zDPmi+Ionw1>JqDZv$JE>be6yPLS#U7b93`ndjiF6b7>N;W@K{mNr8MpSMJSoMQiI) z{5==$Qj!~&+kfs{nosMXgtVjnm%rwlnZ_XKJ(kYAQ zK88;+$oU|EM0w7_L_;SJ{qaFiLSBNIhRsjdxLX3cdOC7Gt5dJJ!A?9sa1t?K+waP~ z>N&r9%|?heYF5>u-+BzoUk!8i6dEe$|1ex+Wf2U5!p~Bq`M7m?QD7$F77W!C8)lUY z6uAe6XQ~EPdsgh?+u=Va()gqilaRd3h-&0Oq6EDW*@wU?7fJm6x0O%R+do}-aZ#+; z2y#v+B8AA&0^8OdoH&V`UR6v2|M}X>8(2eVs7MyL*3ZKi3Yo`l^oU;^I6Dq~EnxS| z3)ACte>oMIG|eq7=2cUpW*r}Z8xb!_<<}}(Iv(9^$W4rXA8w2G8-~ANs|B04hsSqu zvJb{4k3pGos;{2W@;_k>Dbi#Z-i@G!1Gn%=zCf{PbWJKlxu^i?;r&*#s;OC$DeXI`e|-@azWUnf*v7s?u3;ILv#5|(%4vr#k$NlYZ5S%Y|MszDb0JFaz(yC zCzm{Zdf1`DGb}#WP@BBTA0ENQ+%r}+l23pO=o_^BY!K>%r@l`GYtb z0tGg5HEAd}YC|SKYCdlvdo;i=ngd*$~1`;F2ACwY2a&@H1{Ev|xBeG`+1X(K>9;ul`xB*MEL` zJ6Bb+ROOHYCF!}sSY^vxZ?_>PlE0y+M!8L$W`G`Hq^KL37&6Bz<^+d^*}%5N73(Uy zaFMJKt!()xbS(eYtYvinEfc1m8rmKyb>c=;E+W2$hxrrllO~-_u#Pxu2(j!rfLOO5Q zemK}ecC5xoq-c7Zw)*j;=@^zK9droxOiuRvp5l#*b<2EFp?c}~<#n?nY{95mWM^Ah zic>>M&&smXyb0@<#XaMSXLRs>Z^8%+-M*dK@TM6pDAC3w_8D^%$Ek7QMc|&0duK!l zk(aAV4$0xus3g3(!a}>gB*;7HWE517g7Qh6(1IboXGMP^4&|Y7#bk}u1@o6E9P3F` z=}vf4ST98wqNGA!7#QU`SeR@G%w6)Sgn@~I!oUo{!SJiYz=$Ekz?cI10um|xf1L1{ ztG>iMQJDstIic74WSgC0gA2^71~$$5BUaV~y40B^pNU|gu_TH{KdgL$i^=;W%0oVs z?ZcZ&1aYAWzht_Tf!8>K_s4=#X}U&;u`RG6+vpl(tfvSlpEOr;=VrZLz~Na!iVsWN zo6ZhidfLr(&PUdqUe6Nod3ktvG!_(?UY=}MRaLQD&#{t>z#+cxUaq#BcG{X4oSts* zzP<+nj?hPK1}|T}borQO#G4PXnI9|G@I&ZN<0Ad+T?qYi`4&dJHi%#0b?>4#8XUmpgkg7KZMv9_M6EpKY# zxw`00U|D?TVfnC^(|KnmoPa$n=;J%`Hm&-oLN#-vb$oRN;s?IfIKnkQa2#JD>vVhX z*sO=_t~}@3=%Ll@9i8I87K_z>JVJlR-PSg47KhD zf3+@gygAYsD15K#@aEwnkKAWZk{Bq0eX_QeF;%TKtj{GBW|{VHs0TP7zsb%&z#(wp z_pyKH@U5)9cv*N%)Z-RAPEAt(ebkrQXIK3iim4+p~~2RtHfB_19g;K2YV zCvcS`i3J`W{^Ify0Rds)XZFs{&dWfw@UXC2j8AeSh+=uO4bH$@~9x?DNS%g|973I`7V6{6Ki#mdsXKH+$om)ma zg)`Ma+z=_bY*3w?-gx;GXrC-{VZNSbRlh%j_LXH7>S&plsM+v z-<&TBAaz!Db_csza-=wBu(zyiY)%WG-|)Df9336Kov$9)*xEv2R%OMndPt3P)(Baj zyU+OaDRSiIcqzJbn?ivi`8zSH=pDP)2O`Oe>CkKqJv}`GgRB>%I8K>&*yi)sCg<-G z5Hr|0fy?i|wX;*{pnKQ}OL$5=SgnpSH2I{g zwe^(r?RTi}_vJN$JqSlVCxUNnrTjG`Q4SfZI>qrBJ9lNO{KIum-ra>|Mx86y zQ?5H8RZA3U{kG~%2yAv?3I=6%{lL1b^F5ls6Q`}zkvZ6~g@0qW>(wqm>~L&O-)7VJ_uqQ z_nkZ8ZCQMi0Ac>FFG6%OMBiT@Y$FIk zeAwj{hROvn?hDHib~%qW78Ak$L>GFpoBGV$TqeCKO-{#~CS<=> zQv7fLmVuLApa|H@2reHT2I5SYrb|l)jyq1|$i?Q7$o0<8#(bYcK!zbMYyo*9X3O_pUzK#>~0FOZH!2MzUnP4;ozywdzqb(&Vd=U%>ss#utx zPK3*s7kH1QKE$zUzN_6TDI##<-cvlGk9e=2HEQ-CMlYT(5}zX?Qsl8in0&!ZQ;b+q z#*rJ(l)jeN8yyl~Fk$l02ZbJ&%Qof^Dm4?}V)cU&8FNZ5w zKkbZIuoWv84W+0wdGM032hqFeQX&lqa>+S&z0J50&X5JOq|M_CW;_7b#3<~7`&_ue zw6Q?3aL^<_nxB-E>+FM7xhlZBCakmsjdfhvaJ|s8_@Q+Nu(Lydc-^5+)3`z{sc+1x zX4Dhd1W^B;l}U@SZ6`qBj=*EJeI^b8E%C0~3N+YLRyCiF zW!4Q-%$RT`siL(@saw=a$BiC$9D28O9A^7oL(`!yrODE-8R8V2#b`@z5w>fv96RDK z=}Qz^ov_LxL^M`xw-Q^!Jt-EL-{?F{0m}3vU$`WusomX+L)m_S;_7FArLUAJJu6b7}wxJ}Bp3 zIUHBn@xuWkTcCD7g-U1;cfww=KgSnEAak~R^468Kj_NrBS*m70+!Nv6o(~9hJWp2S zvxD|TK4diRkz)E&Q*(cREBMkxGs`ri;Q9Q8c@1YnlN5{NSGCPcQ@-S5zE$q4+w0Sd z?c42B_Mq_aQAeU-g&!Ys8?G2L^c@|alaptrr}NW27)3`%XQZPu+@%yjzOX|>M}No! z=6vm+$H!HK0D^3Ppb|NFDal}t^-MjAQn$YQ=BMAvUQoZhJnD!0r>CzkF`lVoJZNukpBSshnSNYt`pGj0 zmp55NC32*mH=~Dd3pICKYmPb2!%m~N+l#D z0s;cSjARTzeqv$(A#Dt;jg157U$&bFP>4K#{=AE}wyrMN@6p`c97hKcCgvAovLEZ* zKh_&>g@!Amqp@bUqC!vi_vLO8qV&$G*@jLbU>A_Q~SZ~gru z@6{z$DJdwzA|gWY$f#YFm6dgLbWBa3e5y@LNoh*x93L1c1*EvD3TLSzGc&WSjHxmv z9xY-@cMemHXmxe<+;L&Tm!sO|OoNM+)xp9-l1E!x`$zFJUfz?;1r82Q=>FgrNoUmHAkXJD)zwy3RuiZ} zgT8CqNlYW?md|7(gUF2wsk{Wdm8DcvR36sV)?#!reKj*TfBN)k+ZPuVl}GS2TP)!D zjfF#j`Or@%E-fwXQPwR#x_HGFY;dP?rA z@xGm3%5X{6x2`Bybak}1r$<9gt-H5Z@!mVo0{|TX;M<8nzml}a*4|#heD;K0Z$aHU zRE6cqlP9#a-)et^$3J6Y`uw}Zi6=!2BNg7^`ce}!qwUuuxWX?AscxY6kfny(KMxOM ziPuQW$V}RUoBK#fL(|13XOLC>00Ix6SdfT_lBRTcasu@?yBFVdb90lNOqhL86`3_- zCvjKTc#Br0-iC$-{d9SK>_?A6)jq_j}{3Aj*kijQ&x3I=5U};9$ikqeNZe!2U za13QuMd{pAf*Oggb4_)V!e5}x$K#e9Dlx}BXRO$@#(P#isdgx9g2C*%XS{Q%Rl`qN z;5*i_v>I?N6_uXjhf`rb==nrkYEq@XVplgHbM7%`x_V<-FCpI!=P=qBa@pn|w#Rg=_bjM@g`x8s|Rb(+lNfI4RDmxI7#&kE*v zV4FbfiVOG6$w{mvRY&lQzLC%wqlfPO&wMe`c|63CN302w?f6Lk?Hiu@ybBEw7;&4- zFV6_kX02*6X~H1c5TDb@YD+a`Qj91gHMjct-Mf7-;uBUi^Im1v>||1bwlJNy#iaWf zhK%-pG0G7r58znJ7_>Qs23!4z%o-%}FZk00e{uQy1i-^t|V=uomy=3=0VIV-WKXn>ns)C*T||bFYyVq z2|1%Qw5?`v>l@flY`o2PBXS2z=cTRZIY%}S1s(DA@OK^9XZIpusr(q2ni;guxf#?>sqt<1Xx7dZh0T| z&uLIEI-dAjJKGb3%bxKX-rj+qkBT3R(4In-Y^>$Vb#{SyK}SOaf+X-3Ao+j&`n6H( z>Kb9XN!W2RJMO9?0;eM|Apx%lwmBFMRFZNi1km7?|jHp86t zB%&FtQCL`*VZ|gc_4}C4`3P-U0~)1a-sph&P23%Q+qO8mxt~P|pYPcHW*Rk^y7ec$ zz{jVU!M~1zNDt{*M?cX9DI!D{H^-FuA)O)8ApbEQiWQWkRf0`joXQh|-0YJy%1N5 zf`GF9vkL4_|NFm72POYCa{u2~_`@+L-kR2+sdObo*ttk-N z;bF5}Q}<`f*Ezj|?Me0(~=!-o$G;AoN8*e-QaQ&aP^yS{zX$4p2_aNyZkUVcmdM(rUU z-V097ko8~pNQnLX0kjt$GPEp9gAIoGUMt|hs28)*(S?bB*OGWwQ1GOX#pd%v&=Nl5 zv`U}vBg2J63BU^bOXQfDnCPB>k}?O!$K%9-bM?Kpp`piOX!Ce@co?*Zt*z~m*UZce z)AJxC{&_e;_7@{&&!6A%du(5T(t)pLOk#VEX<$?5CIEV_(2_$GNUNtYPZ z8CiGe<&5qYGH`Z2eT2!G_v|)_?KtUWNJwVnY;$bGT5@k&+dYFYk>s*cOmwuJahNQwO=Fd!-w|NNhdVEtH$li6_TwAzL@!&=IsxdSXg)lAgn*YH^DJTL}C^?UYM2T zhqb)A8oF(TTy7wsGvfzmjPU5uqw?`SiuPOBrpCs29AaW(ATa>VAFKY>EcE=`wI^|E z6?g^V!otF7uNocUym=rV7g?L5Jfcd%0}YXLoRqZM9q0u8I!E*iE3zoE0_lS$Um>w< z@LFN@wY6pKB5G=QJc8zW2~zr594BEw{KBd^{Z-Wrv5pjuj3Fa-nGc5lz7=L_wIDG1*WqBZB|)6^5- z;)==0&HH8wUOWZv?-9NvMkZVt6_R%UD+Z#;$;ph8wyiBXMh2OoEWG)ropEs83k(v2GRzrU&P5bhd|(@`rXS&#-S_pOjM} znP)EGghoW9CM9KUWPk}-dHK6o^{UEBMMXs>mO@-4WMp$Qvv@UI+h>lVpDHR$E7-%2 zkBD;%9Bw{=ytBaMyMW!lfH9?-EfIr*iB|Hi;+1Qs zyC;6qKpKA;{n?VG?W-q5opO%L+yfYbqw_gRdKweZmX0QfOlf2xpxtLy$`I|Fe;MLG zOyhri{x-ya8r|Ol*}s1OZDs!>+u!tEf8{Fww0)<#`m5dk`}S|ORl%QV=0A)0L$vkR zO8s>k_$jo1q(8XKpPzpUwtfk>1b+#De`jCF`0(jYC+9gYGmLHU0r04^_x8jAdh@5Q zsWq0H^(B_moAq}kS#4ez@6Nrn=v@M$x@S9K=1X3$1{2?xS1~j^TV`e}%Fv6SVcRVi`U80pqbjs%Rg z@WUf<3R*BUHSaBVN1L!F#>bm5^ml@pWRc(yyLa=3s3OSbLB_{7-@7qZYz~g8lxnLH zQU=d%trB(Ij!L@XyLHY<9w(W z6BDzy<6Ko%R(BtJMQxC!hcC(dN2JG7`~4rnhtVJapBQN>_HDCm0*+U{!D&0h+KU47 zS%pYAz8WDOU-*`n<6_M@LbLNZ(G8Q?2#sfhXc%tmud_2Lkyg9>>paf(T6^EWe_uEu zB0LToM!)cw(?HMIdcOI2+<2)Dm#M(*T>}iYc;9{vCorEJ)6{Ns(KF^8k@~eiGanZ; z8>oEW%2bs(DghfIpBMM_y(lPu0_Ce%R?DfQVfpqXcI%U^Nfv{zDDg*_aKwh6Z$MvC=g;u|wSb72`-OrO1uR-fcrGbjb z)+raOgU3>;-Q=d6<9@ocJ~ahFhMI8b6X!_jQ%fhS*4ixAJ8q`(dYX+6N<`-FCN~)# zT%FrHzB@s4slu0F!UZ=>R*GT@+g@e6BvR~EV?FORQ)??4Nj$3BKIyqh3FegruIodT zRf^L=29iX;Uc7jLfw(nO$MW>) zW`7EoGNq$`i1r#4jwOpmy+c8)Z)IsOHw7c3jH9fBo&EQ8fkx!y75KtTmo*SsKi!!v z)+EK~>h0BR_HZ39(dxr0iBqpMc^9LwkKQ+EDk0HVb9MxwCCec=ZLE*`2<|OW+x}}@ zBXStMPc1N>&mRs}yq2+-vlGgayZ&aXUG9Er#ZolUYtR{?oMVkG6d(9rzF)U zXwpn+tT)qJqTOVso$=g*Wf)0FlBFts|7?HBtkh`qTeU>MUL|l!=AY`dZ^aLZ=3xfp zC8>F~qQ7w5_yFZ*vz+>nk}_MQ!bnLofTCLCL*n1I7~&YM_#o?|a!onb+!6CPJbAz-t(y}91uN_Wsozx0hhI&w%EZ?IYPjgbJh zjIB1(62?C=^;o~^BM?xBi5b3j3kghMdA+fb4Y1MmW6WR<_8nJo8tftc zW_9PKuc&!jfm53$T+_uGH9z~3xTvvm1pMIZQ4E&5-qqX6bj~V?nSu8ZsDXy#+qkm< zK#oga^D0v+Qcjqhsj~BfhJoaP=HLl4x=p*`~7VFM~yf* zREeUO-*stD5SDXZ#gV>;%yK*QG@?id3@c2aJ|0zG6+&=Yxq-TQTJ3{@7E3v`V<7(BrXn@WBuDwlH2R;{S;oC{VTk~^OJGPk<_%LSt9y&Jr^4nV-!Kq8ue8eutREGtXnk64o zUl4ejR%aZi+P@rsv`cR`wr!f*o)pXV0 zf89Wf^A(nmSHPTOZV|0SfR$tU`;gftYNGY6zkw8c@(j^XaV)=ZvXHDzLHYXGbN~`Q z1xC$FMotIU*0-zc%;XcLdyW& zVFx-F!o4fnCrzkD;vn9p0;1;|^9p%C53XyL2Zb>`t`+H^vw+lKxF0XNUmup*?Ad6` zG<&duwQC&lA$p)Z9p83xXT=8+DZ9GlfmNW#&KrJq$5k3S?K5lZfDqfP$_!1+e;42w zpxOCfNel>+{wlrsuWf(De8?ZlrhonfLEZmV`mg=4X063wsm;eA}8I56^0-I2XIuA|SAI*17 zneeS-$i&N>N^E`eC*p|n?3(>NNZ(#`4e;=$0};IqJvI^$!Jq)4rWMe&L`Fn-q3x8r zANwSDMtk1`-J83eIPp$kx;?obN7vc;QoDaKnsnCVtx^AyZS9l?bsR*m_+YDi?qP>^ zIB;4L5g)*8%;q(S^7$b3l|7Qqgt=Avts}czjOU~HUk~KpTB$!Gwtv@f{i)}=TX^wD z`mYD{A3<=iBJ%IW^cq*}yELj7XSOgUNM`DM9N#?mutEi*wuKi2DDdp;>>vm*ybglA z#ua$mRhmG2%2--bg5)n)r`c~@0pd*CPSX++kbwc@SD*V$Sih~JW5{5s`;NALSdSHR*=#S7EU>r8|`mk!x+}7Whih$|bQj7n(n&g?)z`G)no# zhmDNtkXop0ewLthg=+UxE1K)!J97mS-(6Ttd+_|U2Rk@dXA5pHE6{A0YoOq-y!-7rbq{x+H(`j z?YE^q;GRV-o%2>w+~4UaMVOAv<65%epc#PaQ~m_mzXfrH9QgwEV*S7e=?Ok$KlVA4 z_XIPdr1KKcoEnJ0dPUtwTc_^1Z-&>moEp}s%YG~|;fIq`h@QQP{0^gaaFUgAJ)53S zev>?mN@j&3t@%QS_b?9KyL!gF#Mj2R;4`XN>?Y>v$!<;k&t(PAyjli!!$tzrw))sDVq`SD;)1fg&vl1|TD)j#YAyTF z;qSR*dIl8ec8d_91-{{j`57;rSE$ZfKE3@dX+mQd8zWG{pKjV_#o{^GG)f{)8{ z%-}2I)j0w31|U)y5DS^5>_B3$sLIiOEC4~y`pAY93x7XU@x9<4Y&BHfERRhvQw*b0 zoD+GFs#vW3kx3ATSq3@mLz3{wt9|3P_GK!nhgtiVAw4PkJH0!-5q$l1tJ2B1eB9g~ z=LajLZ-N)aX4X;PBaQfqlErd&Jn%Cl=sQ~$@p`5I_F-QTi7*qhcTRJ2b6J@UQ1OmE z66vUvpQ@U978Vp*v$P+rNC7ybzQw~VRO=|k1v>Uf8r)~8L9IelG6t!%YMMz@-scL0 zFZlSdt=tuk_ol{%J#ht6%>5{m;_==?bJczf0bG3oxsv^U~Uy7=Sbj)j&x<;pFJp)bfmn z=Xxo;udh#oD;O*;yGjBAiE^nIq$}}_ZM>HoffP8`yA^3rVk+l3AU=rM%H|o=N@cLH zw$3B+W{kvQ9GNAdKx^)4VU6p_xtcsO7}Cj^7?=;vvTtJOH)VZb;aaW%KWSI^^}NUu9mZtJZm;-{o9`0%z+->{+XSZkh%g9Gi!I=|8WshYqA9AeNw`sd@LHuHJ-)-wE15 z0HM6RJR~FpbeB8%)%?WNRAF)P_S#x}d^|2LZhTS_0U*;vM19#nh!M06y62%}$Jv<+ zkbp46^>8Ds?=}xasP8oj5ddpyZ*Ol`k)NLrm~2JG;n5LmG`W`o#q{g=kZfpEQ`0~- zbicXCUwv;&6F^~UX`m!AJ^%s$AS3DP?Tz<=tMbw5S8Z?q0w@f#>s+%Z!8CXgdeaiG z5}MqR3&_zIIBo)aKP6?zMm1>@qc?U#e$*$QfE*ejkVH4&Y;5vxMmR%^Ydepj|=$9p8Ne>&eB zwv2|6-=VV9J-~oso;`c^H3M)P+Iw$&fsJ)IDg4#VRaM{Sq*8Hk0j<%1TiCm zz>5pFnEm~IsxUy)N+fS@R+HsgIIqyGX=rJO(zw{!pd88xumiwjrm8Su!EjU04oP~`U`#Y%ZXv~NwYE!N zO^0&}&^<1W)&ZzsvC3A1m>Cx|0jXMLNCOWnB^>=;>brLto&qCuPoF%QEH@C9kob1D zV1__RQ8C}?5rgJ}Q@SN=rHpyJOaim;(~(q8M~UdBQq8&_rjUY7DT%K!k2P-=o>{lbcYVdhoskVKr zdkiYN4HoXC;bU<*?li3m*XHiQY|z{6W$IVx1oQBHe~29|m#uIKWI-h0Du^4u{W zEv&GR7VDkybE~;Vwa^M$^;%tib)OU_-k^wxYUeL|X8|Oi95=@Rzbx7mya&t5$hp9Jh{^=M~S!w7^& zOw^A+&ypHdBo9zlASq0INJZ~;@UsxSQ0m`Q=y6K!=u<3MViR-n&CgQ1#mh3!zo)RE zIPYa4o|8LG@yDFOoGIyk4ikxxYmz!Uu)I)%^^weB1#1ku!~J(jv80M5RT5IAm)VIM zZ_E5BMgS2#{`qarrYxL_+x*dVs~>{>3aC!a+IC2;rTK)6f(Jc220ew4R%LU7hQZ4q`!BlvTxmw zoJ7#H^q5{O)LJM@ERJ3yHafaOzXK_4pyu@eaZflPz6HnAN(#k7MY(=7QMpjlOnYN2x{ArIbBNOn#n<<8Xc*ykO ziFxidj3ayB$nb>P<|`Az4;M_eWdiCxNk|J{cG}lg;9;1kkp2z5{=Rk)#L53*fguQ| zU%eU~9o?O8VHZ$RQcA`GaduwB`*2f+4ghF_MIb8xD+4wL#;nSM4L<~Q4nB|vw~89d zsO_=v8MfmkSeYasAmHS50`z;JBY~21$l98TGz73Y5>u<2q^3*c;tgIH4lgS^EG;Xj@6#s^R_koS zbm7RG@3|3^T=BCMNvI5T5eVAWIY$t&gibMZz8Ka9_XULM5fWeteqvq?@;CV!5Fa?y zQZixWFWDxqF@9wlCbg4RQGvKrom2O0qYtcY-CMG=ci-xMks4jPI%6-x-0BM+V?<;m zu){A@F#_~NMFWrh)c0U!5+7*04$@>k<2}1t9rL>2nobQ#J-z)gNs5@rn^1e0mU?+! zKJfMFH|Lec7y5w=%w&+AoJU_yl(WP-_plNN$X?I04rQ4OpDWbjTNOC2)N_6^D}bLG<()SN$J94)qT|6$O4D=Py4T^~dJl zeo?9wGraNP&g~n$4+m?!#-b|7JCm;%-){V6=|PP)1a9%`JnKZI0N#=E+Lh%#_His25xuO1kZ z|5w_TheO%6Z%fFMea2Rjvb=Ua*_S48Y#}qgYu@+weP4ZVe|*RBJC6C|dFGhwzMq-rexB#L&g(qS8QOiMTi~@hc21h# z)0UR7^9qo8;s`Xh{CCTCcBO;PjEDJtnG5d@{hDaTgBfXJzG+(M#(L$vO( zwK$-!uRlm`GOfo-tW|2SSME>Lfb154nh0UnD=)p6q8<_Tge?Cj)0@>|k5a7zpDcTJ zqD)bDFV?c_Y22TqHBtX0_8SwRxv37|vpkvwal7LwtkgT!3YlW_?qH;o{Ns>Bq-cjH zKov4xA}Tj_N+=qv9^lQXL##jR@w6#cSm0%L?ptgU%~9)!@9J}nhJPE2I3DYO$JU$u zBnhBeVRUK+gg10s_$RxGwU&r8RxD&)_5fsp*FJi~Q=(L))^DLNIKa`_*_or%2?xcy zi;GKzolSeuy#uWDH=e^eUKoyzhJc~Tw7tPA8>NjYm$7n$yu2mh^A);j7ZNV{>Zjpb zQ%jNF0VOtd>opW`w`8H^7Z9k(+DC6jI`ejdHnf#WLpd_1qb=|CLlIEOcUPd_?silN zbV-Jm46TO9iCN`8JvhjNIvB+ptRY(dqFjJ5WW5@O?;kClq>~8~+QC=9S=%<9eZ28T z{_&Sf$OFpqp^Z#s_NYhklbOm>Vn>y*l~q-zv3<~yMx&3<_QI_cG}dr(pEKvwX;C42 zW$%DWG=!pm@UCMxjsvxA^ksKd>E-q#1zas-RrcY_BbAq+>oa(kAfAd}zH5(nb7A%7 zr`g{aoQIXN7F@R~i2xfk89-SR;Eli2&s0PO#olr=zC_XA{U) zYrvmkepO97?|n4G@kRhY;1}GSgQU1CLmslnnGGaGuxX+mzsIax^_1Q6NYxTlxd0tN zb=C_EsQ}j7#B0tG2=90&)lJ3p%Ruab9FLrweDDd2kz94Q5v0ut2?>Ose{G86Q03~&Hk>Ay;8gHIFc{25MS_r-ju*6UD@&>wD2(g>fw%<~D z(K9i@&B1X=niJS~#|w^fadA0U$n*0@IZaX9i;oqEOdAX-o?O{@?20>`4FoCLpVEsg5y}!2F3a;H(bRgCZRQ>>-+l|$`LfxVHlHZRLK!iH3Yk}4Zlhm5ow=VyrORK@*r}#Wh-A8*b)xS}_q%uFc+AF) z75wv>z``aS9(!;dT3KB^5X?VJP2C)0#y$CMPG7$yz(DX=zpL~U6(b|Ya~3t1v@9$v zJUzww*1<$)h*~QoWWPp=%H8)7SL`nPGM#MzlU4y?D~~z?fd(R^fJ=Y zhifi*c(h@qB_w!QBV-tvlCDgj@Rl)RW?H;P)-Q|Or@Fv&uz~YN3=IvnoSnKwkv?`^ zL9iPYOuLPY!#1hB?dtmYQHv&Cg)5fAkL;Z>*!x&)mT{5S-AE4X%#5bJ7xDGerzj_k zS;|rJ-;6+B)z_yc)e|Ayz-!pk@U9$(4B!1IjN+rq8;iTC!k$fKac^3Phar%pIl@;( zfD!9>J(XupvzZ3hchQ9hGlBAt8!z=B1@i4Y9v0>Bn}m_^meH zWDB#oZVxN|YU1TqLwe6~%H)}9oWH$2=|w?T$~w}Ufb{9On(YaOXlh-yw?82d%d>7E zYHe)|Fhf7-*Rq?IB1qGmY(quWmlU_B@yNIu5*?M|Sk|3>zIuS&a&jEp-D#Pmne5Tr z$+;yZ^l1|EM0r$Ek%+J`7-aBMF(%>hp>?TEZ?a=$BqbAFqMu%~_i7G#$jZv9r>ECH zG;}*Ij%QL+7qJPx1<@I;?d^+x3I)!5y@P|*pjtIEJ1rv`RMY|xy$~r&A@*(L z+j%`u6YUl8Ab^@Gcm*1ENx#3iJ9}!Dr>04U)~~L$txZ%^R6|Q^124)gjtB{-jT*T2 z)&F@tFTk)Uiehtj3HHyin2HZCZG?6RNV*F-6BSnu3iUNeNus?>SQ5<-=bqg>qF|wL zHQC@=ADg$&I>#P;C>0fxfB>7UZ1f891+}QDApa%iTapd;#@Km}J~qF7;?A8<-*W++ z{T_Wb{N41!2AB7mS1|=h=T;zd|P*Dv$HV!5Kl~nvUtIde^ zex7gL%Z!4W~Zj6mi%6fuN)lTE(rQMtG_)hQjZpH2*u_p)@FQGFT?xb!<2QB>`GY# zST!3;3X(&iz*V5i3dqo>)1vJkJ{wpZ842;kS!}KrST&sjBn^}TpVrI2ban*IrQ$3x z{4|8R5(oUYzBpr~m5LL!U3#uAfvn6Jp2!0ue}`+mGfj+zLS0r9Ec>I#ND`E9*Uw~Y z7=Jdo70E-dK#kRv6&Rxb*r&SGBc4r9k0)_Z{nD>+^@>6Yy~0v9JumODP7PO)Lwa-d zDL3*Cic9jxu<7-m4!Fn?3|S6Ni3iDp8^D+%CP$ms03j7(%dSqFFm0QL5%33c=>JL8{%*#C8*HAtj@W` z4p6~+b=_Q~OuV?8o4fV+eYCia$v6+#P8|kkD)|uZ$LAZ#_H{+8*+_HJ*#K*_? zCjU|EwGl5L!hR5z5~I($#;p_wV&3|o4QCGz51`eiM(^!-82vfP>d^gI4*Wlr9h_W!(ph zfJy2({9N@s3bezp+pFgEeJB9FQ`Z&lwsHy~Q!ddX2v@+nuO$ zhP9I-F}BDb^%i3HMLZR#ys%tN+R7P}wlG$GzYj2r)(xo`l z@O=ReHyMYSX^%gb$&5rgQ_g+*RCwJZEDUw;NDOjK4w%%O1qVPM{SF2a@6u;>9KlNg)N#g5Fw&J@b5++$jZgx^8~O3^q}=w79N zHN%ovXpAPijXaN4Q-y1IdGKlHP~*a?_vyw*XvgvdH_gwpy;<=@J*lk?1a2&7OWHNS zZgK-MNxPZ3xSqX5j&yn0xQaip5*_jwE2A z?F(6|BcvtK7yg`V#_8fkrO>@iu%BwzsiQz>BGn%W1ycXj3oNkJyEq?w2= z3lVzz`Wz^Wt*XxbZokm?v|Z;P?ZOR&BnC|Ga=`D-6`bw< z+9fcgC@O^Q2SVT6ZFXT9E*yqq3Pd;`O9}C3O@p`k3(gm|j89CIR0FRPF71u^?n$7B zrwCv8d_0a8dwYAGp{~drf#0}_l#~=&wVMoZym32wdz^WI zkm+TiY?~uJNqfnfqL0wgDTeHL1LeEDptv}m_+~^RTozGLlpReuIGi#HH9-f3BiuW3 z!c{7p1bRN$p9`O7*V6Ln>W0DrN4WoJ=;Cgj9Si?;X;}qA^4;|H_W_a*N96}n#(HMr zZb+6qny@O1am?Tj=W7j}o3Ul&AauTc-tsvuhzTvuKvT3MBP1`Euei;%=kj^@?(2Be zcG)D^&ULlMHIMiMLN~77k!;^Ne8DU3(#nGS!m{wgV;p-o#PQ_q$emKWLM|x@10C;Q@NdL_Hh)5b|MNEk zzd(|I2>N03SMk4?1|oh4`eF0`7ypaHgvtN+`TytmUmYd{|04c}@Aktc#H(B&^?!X@ VfP|z&8~%@jL`TCwy+jRj?Vl%Xz4rhB From 7b3e121fdbec117637de72b418f6efa983a0f765 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Thu, 5 Dec 2024 02:05:54 +0100 Subject: [PATCH 20/21] tests: Update some tests --- tests/plot/format/ref/1.png | Bin 42386 -> 43278 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/plot/format/ref/1.png b/tests/plot/format/ref/1.png index a7a292522ff64a5c2f8070e9e4fc11e0278fda73..a6c537203102a99ac5211c558049ea5cb7396f68 100644 GIT binary patch literal 43278 zcmeEuWmMH|^Dc;@k}8T4f`K57NJ}G#NJ%#e(!!==Qz}S^2oe$-1f@%)bAy5)D4kLp z=?0bVGaE#G-}PVTtn=x7InT3L&$Huq$J{e>Uo&&f_E(g@MNB|RfP;fWd|Uda5)KZY z2oBESoMVUJotI4SIdO1OJ8$0F(Esq#3_oYjC?zY)$cdTM0MtPJ{Nj(CnpY-#{OmZ> zjVD2=Kpi@$a2_G;Kxd{wXO{7C;xW{4?npcwQ8@Qh-di~E4i1hYD9iuu5wXGTwTi8c zo`!~5x)sauAyvdy5qsP#()q}!sMt?>^RCXFRMU+O4INa|a%jt4%wW0WYTMJeRjbV` zuItK()S^6XL>XzBYeDDc`=ZU!jZSTKHvRq{ikmDeaSpp&*Yn!@7gONToE&7|&K>Sp zd4|w>H(}a9DX)+Bax~VeaA+tyP5_$Lg|xhxBKCSQO2B=ogbG14TT8H-VtQB_UjUG_otJwzvk9pXb+&DMNmp+Vc6 z`plM;h=E00<}JUnd*N65jFeZ9u#ef<`sXaAlN#I2K|h@}J{$MX$yuxvZ+g)3pq@$=n>YII{@UF& zi{9cNtNb#C5_GZc^S$J>Ytku2yk5(s&RX_1EzoLL$3C)b*UloZOi-nj3VC_S2h&1@ zlt73?SrIC9*sZowz2SxToS!7{&R57I-pTRK3-5hPM;8$o7&u9cep95ci9nXx?0xeTJz^pvqT|Qc zBeGgrTE2h(9vOKKyMn&#WL}|x;?2oGSj>#f&+*gPX%zoXbFiX2Gja+CD{7%quwJSi za56(rP1O$ELU_YCgG>~C3_(=n)NhAwV9jdl3UjtwOFXYQVlvP#9xElOtpstz z%@$Z5h{xDeAx?O^h?U<*eOsfmv-4IEO}PHA(R#8aslx-uU?2%Zlg0Y4G38`ge)kP~ zHMOo{n~}D*wvL4}XV04OpzQ4INY4v!*^SGt?~acf|9#3QA~KTCc2t_eBk7@#=T13H zbCz9CevrR8}(>+jQ@8R2YEZrmPQ&SQM~ z`h#!ZUP{Wy&$nOuF*F%?tn}pG0Y1-)j~A_Nm>383-dN7lU};a53kV7_X=XfW4?+c{ zpm<&X;FLGWg;{r)M>DuD-z?O&f8=uz8jUXNqrBnx(9+V90XZWm*Sy{4O2o55 zG^-lb*wLXA^mHaqPfy&Kl$42??rax3nZpBqyeXhlsGNx< zf(|y_(nNd9fl^hr@v$*R*YJpluSjiWM}CV;^N{q*!ZvATw#mny)-Gcr|s9@`}p8HIjw&Crjf)OtaY-Dh3Y#;`d@eNih8^66`(-kmoIM?GB|ImHyhZw!ll`B`$ zRz}@ZQK-I;UPr@ODGV2ehFb5JyvxnC{r3Jo0#h#b&q?JzGvy6nrs5A6QLS_+Jb1!T4d-QUYtUR447d ztgxa0%lczKdv=F?161Us7;Jf8;SYpnrv$zzEiLWk%apL*RKd; zf2A+%G_Uh_89ryp$$1>6Rkpupf>06t#drDTVt*7_8Ej%*v z>RH48lw)IKHSdo6*|HuoRky-Tb4up+?Uw;1B_&_<&YU@u`-A-4xwVzK zzJ>%f2)pryYp6zke*PlF z;H%hlG6wm~>};pRBFO;q?Qy<`w+;`aHkwrO%hM!cPXMXhmZ_O{*c_Bhrh8|5b8L8H zc_tiPXxT@P47DVHmG0gUc<{;ZWUg}OON@X*JJ-&W6Joq=TywOs*bC+j@0G^aLfY|a zIy#y5Nl8idHHnFIM%j%h-T=&l=Crme6VK};*WG8QEr+U}MvrK?f{W`YTuJ^j|K`F< z+rnVXd~d-WHX1(b_)y}Mp`js}?l0lYV+OXioFc2BI(~7k*!zr#ML<9+UQJOk-oBnx zqpl{;q?yUJp61Bo==ujrr66pEv+7w|Pyb5k!>`4~Bj)4*N!4mTG(yOZt}Z55ZZ584 z9pLN)t}3z_KX~wuy>eu93f%jEjzd=0Tf!gb@mwf2)Fh)DAxGb^iV>1gmtilP%#RLq4` z0`{rNW}IHQ2S(F^S`4{JVC$`IVKEa_>A=d&9J-zUODX)e=7Cv`PhXn|JGdjj7_r8- z=a_!qGFU4;`0jt1E!Kkv?syiuNg{N zTmMM771Tk?C@ic;Z}HE!q@F%~`ug>2rv;2^;e(y<29B)kY;}}LOi&L%OK=$tv{)Eldgirv1>ebSPXxa!BFd&itV2wu2UdaZ+FB*mF1(>}`NtAkR5@@rCO z*^jFW+|)boF0Qh=7j5ob9Gnq9%JL`xqd@s_2~E(+4pe&SiOx{Z<7Bm!(~ zY{s?3#jA#E0_2z!5n0+r^WU|!v?ys$JhJy<^Et26{xl4kfDuJU9X@>cY6mEK2?-5+xo*-F!(O@7 zCi4@6-N7oX@pjvDUg7w0qd&;qL%oHk#+fl!1xwEIIi7yO@q921`%EhRpsdye1n|CrXzbPS1FWTN* z)lLIsF%U;3gv|GsxPSl6i}g-FWT*B(sl!=Tt^E7d_U%GlVwSity zF0)%N1y+gTqZUG{WH;7)`ee#e?R}h#i~COHb0nTLXhb|FwWthFK_2(_k^`}CKiWH_ zy9k^q7=3Rif0mfa`^X*-wgzu6;|E*Cg9x!J|FdIPt{>)_;M3(bYG!-#7%pEn9QzV3 zaN@;vR-MbsLt_x_V$(A<}?E1-n zM;_sNTLBKO$heJ`&-z1mw-bn9TnkX0va)iP%FUbJK1Reef{}kGYGz_$LJs>gNCi?F zksvC!9m^c&8jb{tx=+6GRm;|!+X}k6#zm+aq?m@?(=z^aAad~nx*&O4MnQr1Xro}( z-2|W7NG%txW<96gt*x!omTEEuDK$xVB9ssp27U{Vmo%EoEM%IMf#Eat@W@C(@q_wP z%*}vR1stXm)yEp%fDWbex)Uzrw*?+d5`Xft6xh}~T?>->`g$$d+qZL`VU@wdJxi+~ zUq2j)2fF;*zJd#1FqQxM^{YW_Ah`zOlZ;54k=m)+J~&04IW6mbQ<3Hs{l`=uJ;Hga z<>67m-+;AA03~X;+C^4bun=qO_fdCY?Q882c4N1j3D}CUX;HWDnJiZp)@1pojrPQJ8y?ze!Ka~-JK(Z}{7`A;`(*(~nKgJbr3V+W&c-ec(UGiSX z`8<7UWMssD>z@DB*-S9{6rKnZ05gBLGI0Kz9YGs0)+VrCOSBXhYa}4&-_}0F3ky3P*!C8)HzZhJR!AFH){FBuNGLdO zCgjLN{P*)S?dIlY#zg9GQDrf8_rVtc6V9?FrV+_Sb6xZ0vFtVNA_q;}TcYY1vR%LC zD6N|!v5C`}##ZFk&(Gjg%}Kxn!RM7S+(JLbnz|xFwFE=s{Qa?7me%rQ>j>L4%3-F< z%yc`9QMuthAaAX$jhXC&Bg$eD;g^$@Enlzs@E8x&4t?+Ap^T;RMpTL->bl7beiqH# zn*pKpatC`U+I^)bfoot8WU&+P2j(Z8r!e$|}S zzuLj2|B-sUIWD`#tUdWaRkY7!^VURJy)W6N$}mX_pXl6S)h@i+ap}?}tHCetZAPS~ zi4&=?PB`Psa`I~5EL%sp%OdZfTRSzzbwNl@|6bRjeN(HAY5#JSm$%4jpcE{v;MC_J z+HO)%E%}3E?$EwcU_17O7vp@HheyqK)tsT@lRsI3^xw~u3gP4-$U03<9!+teM|TlU z#0P6g*jMI*`(a4c`?*xMFA8Z`#)iPS4x4@#?Y0$k0 zAcBzV%GOAb$gF(D`Z(43w#|m?&4Rz`F2@QuZ0u|<94H`oHJ|eGa^&*#RKG4Wc)wN% z9^pQUw201*ue)LW{ZZ81U}dFcU!kzDa8y*(U-Nb84`8qOR_27TYmqC6kH26y)L&w^ zJk#woKJZ4u*SsV3=Z|Vq{Sw=!;Gchc2GX`fmjNCWx5X=dA}XFt`B(vPn+3lz^(!l2 z0nNgjnf#N!S`E$3DSsr;=yi&WOcl}7(=#6ba#5=TN0~VY^DG0rGh`!TwW(plDabnt z3jaOp4*2g4L08|)%eTnYpU=P2^w&cp z_ihtedHF5_H@9-t58wpikkgExPuh%iplE75X0*h_aA{8jTRmd)87xgEWc)n_WN|Jr z8}}F60Qxp2r=(0*)Gl+(;!o|%F9ruvMkZv~X{CIAt2Xlp+yo21!BWtzGv;4kv0nGR zW>{-_jhDB(9{@!5*7i2y{FdX7?;rI&x3PEW!CCd@8U=`3R(T&cqVd^SU)L?R{;0D# zcfGlY9FP;)+Fl*ZQqjyBX0CfPj^KMMFrv#1nz%`8eD|i_3U$NXXlopCg}977MV%rq~s z)~!X{i&#)~U=@#%*IQrXf%iY33?WB{!Gi~hCGWwtqD1~4bf4#Z*+RvP<~2NgqGFeu z(vaplZEp1XAw504`Q2uPEJ3h@*c(ne6LFrIhZW?-N^u4GkASso`$F#2NeI(WUEB`S*!lyA;FGr zot7Ny1}%sZ_Hk0S)eXy@+_5aRGUT#3xMfK9WU6RsMaRVrLPVG)xVbW?j5d>HI#5uE zz2diz>XtbY6B4%Ta>WiO|2&R7moCYChKwxXXc{l+xPyS(nzgCvBy)E8TVy-2pnC$3 z>_0YY15RzG3PN6dQkrt>p(;p~qWDayUk!}Id?CS z@bJ*lc_S}FnDHAX>p)JId+v)2h${>HEH(%h(aC_g6aw1ZisE$%NJNx6%qaacTeAS9 z4J~3q?BjBN_+Bp3EC{B=BNJ#hmE6^b<2-NB? zu!tyW0W1aSwLA+*A^eU~Ss|nKrVxTuJ85a@mf^j#rIgH$o9i!G9Qzspx08U&uTf$u zp3`T~D#Lnyqi>tuYw4%8D?QNsvM3(800@25UT0>qY-g@SP2*;GB(-JTiC}wH0v1!8 zUX+-aDA=8^`h@h@6g~tug5CBPllD%D{_*3-Mwok{aABJ5*$E^aHZ(;dWJ==XZaCQQ}M*Xf(3!o!^S*aOJ!f{^M3p&KAeM!|F#zsf`J)Br?+~{j`MTm)s zjdM88YOubb$QH6?WtbMQFq&BMAyUPSV@Ye$h#E?C#hmgm&9l(yXqmY_qVuFk7_WaBNzS zrQJUu)wU zUEv>>?ssi#i&3Z8C5}Uy*~!Ui`su^2kZVtQo0~N?H0ruorXi^PJx4}XR%2`H-B&T` zCO2-}XuIF5yIfOKW2>80Uao`I(9q!E<@H1lmKGJ6n3yoI(+;`+(To*I+_NW7Uet%j z+}+$T4b0C21CJa&EUTcvOnjRcsuAhw>5#sp@Vd&v;^yX-h>_-n8Zv;)xd1^Sq1$nC za&i=bQp^0D1x(8CBqSt0fByXN%cTLRAcJ8S?cP|439*8@GSR-{9 zKrQmqr<+M9&z*~W{rcTfeGQLhPCOUV)pe}&{IXOj?tXlkT!2Pf8XFsPoC}kJU_UEs zuDaT*ZD@MB7lV1n^(rF5^5MgOM$Jl2rcWyx8oHl!7b+}AO^l7hUcjQw3ZQ1O437E| zJ-rWT;=asy+nE>{Un+)kjvn=mkB^7?2C1<;B*H3ZX6EO|@x)7U zgF}a$+-)!I8@J4-nws0Gjm^!>#9kjS9EEa3SjG0XJAb${61ih{l8u{td3;CU|Y7ya^5Jj&M&&)y5aMF&RfuS=G()niDyiPnQ6l(0Au5N4_WdG(C7Am3m zvPA>(?~PuLA-I85mMZO8Dn?9qjBv^(oGuA1lEgV)LGE$qE(xa2!{ekNWb_ z-~TxJ<&mhn&UQa0C+k2S9xwk9^n6{2B3Or$7(a7Dygfb=S-!m0;`blD91kOxPwtTD?i;1C* z@$RZAARv%D@-eNS>c{#1?=3ZARws5Z5Fn9~L--O8ZT(X`D!ruZE;KjyFH|2{ z3NpHLHA{n~SLC@<%7 zOo>V2(v$nuWVXvo_yd0X)rrZ#c&d|o#k#oxsK5=|CR}|swt@6z8*cwrGjlpt4}xQdHJ?|5nn>+{3X4x#qXk-R#dFjEMlp43Cmf)7DYRedF_GQftW~P zq~XuE@RmYh&%>6!o@L_{s!b~NHTONT!<(X!EVT^rH{Vw`Ysi)yrO-W7wL4M>Sz0Kl zLqTIsI#BBJX@aNK_kT__ z_r}1t!O6g>1T1ml7+Q!7c_aJ3p76owWEHVL)eriQaeOfSSw#x^wEr=G<8Ac1Tg46A zazy3A^2+)R1~GdJ{{W`FS}A4=L#3`gvH9bte3LEF%_8zuY;CP*(>PDFKx^AYd>J{w z@tk4VkyVbKOt;&mT40A~IN=3xpoaMZ_H%wZYro=iO!CV_;9!P!*?2p zM&s*o&FW0y_5$aFKC7yZS#Ex*9kD6uYaFxnsMK%gA{kx6_Yx}`kWW$yp|hcUkvN0o zL9vF*3@g2orsIrye*YW`r8we~r#yS{m@UPz!>1*4Na}N`Nqk-UDso|j?Cz)Hf;7$6 ze50cB+1)b*TC8G$_c-dzG`d7q8fiPeaS(hL8qi*KjOfeyN_V@k`GU+zqhKNe)LV`E z#BVG_L?LvSa;_;h@ZgQ^#5Zc?xgAX(Oy-prSu})iri-)FmY)W`e)cPQLm4 zG2d$I)S%BJS2&Mf{BeYg_{oY-JbL@{hgd5vN{fPCqvMrHN{l{px~2tMrU=hlJSa)m zva{hbH4ivr2MxO|gPr255!4y$5JEmv{+aD-k{wXA0j~Tf=d+uV_7XAJ&mMOD@Wv&gB+y+^77H9e&bj{&b~wc7pJY;<0D>dfB&H zgFfE7@KXBA*sq_?-gyZsOu$tLt*6_cawSOd>gi?VcBu|q#6zCVxj9SDD8(gu%0O|2 zoWTXdQO-Why(Ln~BMx|U@&PqZR%)H&v#vyZW!$(JT2C2R>rkS>A}k^@BzJ~ul%q8s zebE#l(u73&$Ww9g6!y(o=F)k8s0;Tgbu8;N$dNu)|A6yc2~i<$T>v{fyGLbQ>Bo_u zqRIUWs~{c;x>F?s^D{~bt>qOJubWMt$&30EI$Vxx9-MRR4ibnZ5w3FZ_Q#(i7IWRS z`QUxwr)cwTVf(l>-?58q5dm*0IJUMMF+I6gPv5-HNA=(%@e}%!8S}YMp5)#AL>RqQ zjPF%r(DC3z=)k>P3gRb?_m)JC5$yN{GBpU#J`KQ>ptjCc3!0rf?Q?^6A~wpdDY4y* z0fn=)={~z*RdoIGsw1Ctd^@RcHKFmEGWUkFU~*gi`GoVovMyX{JW)#c(>X>in#zzS zP>S$r6`>V!%I1}vpwGY;$Fj!An@CCt!w%HP)FP8d8y87F*s<1de)>vm)o75D+G$=D z1ahI|SwH=4YWTb$HHjH1oBK;?@ApJ5#3lGY0t{$bw(dO*j~4X|9=^GDH%-N~u+|O1 zr{6xMk)XqN>!u&_P*KmrHR2{A@y8rb<{X)6@%2#8YuDZ8Ps*wY80%fm=uGcMi&#?jYuWoYln*pIMGhquZ*RSfoVLpEDb?jjHiYYwWEGzKrmrtZJLG zFP1$^AXr+d!!|bZQTN+OaD+_KxzP5hrQ8`9R&jmnJ!a6O0*YVPryK8e^bOq0`gHoK z_(iTL3t_y+I4;fCc$@?J3U!&n4RX#M+4H!X!Y{Wynh(jnRrwU7a)IS#tsxF)lq?S_ z>FHXdwZW}7t){Z{WNCO+bO2boj)jKG(wh+thMIB{eAgkIJ~ZSkNW&gF?ZTGsXF9Y( za4QaprXlg26S00_PY}}~yyTx{RTOYHk@(k>o`+In_c(&J?vIw)1_mWp5O7ciM&3B4 zQ%dvl^Mrx^$3Uth-*;9c(0NX2%mHut_=CeP8nJMukx9Qa=rvVpI6bA5d|L93#|l& z+k7*b=q};g8k<#Bhu(63@{{eqe>JX0%c2XKLwkv_)bbn`W(=Q z=BrtyzPJ8@h1VbOE5$tz3JMxBC$d5y%qCCV!R5^_ls0FO_~5TCxD@!xtVe^6;;Rk9 z(lTf0cHhL4MxtG=xf7eXUSGf7`KF*9<4HhkKqBYw`v_Tl(dCxCBj1u?y6eo8rG_BJoG1inu+HtNH_*akdyC|OqusxjpM49#lrC0c9xlV2z zUZr?CYciGC_iqcm=aPae5s#~I$a50#zrW-P6IY>N&;High9;7a8tpR4aNXn={qSQq z3pMm!T#1O2`&~*3!>dC(7yPtT`5$_Cyw-p5;zjMv@AUH%kB)Lb`P@l~+c&V<(fg2H z%t#~|FPD6@ua=s;p{r`V-B0qD+KWwozo6o>vKxGS?|=UK#Yunt20o?GjbuD6`Z&26 z)5Q_(ApPHh&ma>MOMO_^^Ni=-r6ibr{Je9@74L((cjC$G-)}u2UEou^kdmm^zLvF`P%COe!j~`H z_2>h;615jV+?$^?2;!fg;Es854ewCvmaU^B&&B4+23%6}ERXe-IqkD%YHDf*ggB=L zTY&~P@c1$ANSN72!nF7UsUPc3{3V*?$`SK6*yu<%|S1;zZxk z@>cJ$#&sEG{b=f%(-$xwP3#beplhNApTo5sW_o7Vv-EX!&$yDgl6}Gu;9U|K=LvmK zUjC#agu8I0XqoTKyCM9tXDJ(r!k#-@!^2ghYzvbp9g4AgYB5su;Wny2-h2~|x549# zB=qHIZ6oP?lo}Wmq<-8s@{@M?oBYZfsqC1a-aljlsLSM&eZLn59>+Z~_F@k25c30G zcjZqL%(uIF^}ItvLo-P~o|2|9AL>e|*~nsGW?i9Qq4z_d!F#}iJbbTrZS^EX5yg!^ zybPJ^lSw3wafMh9TveSRe(ogwNJUc9^?QVaVM_6R>9%#x=J39wT&(ZQgT)dQ$DYNz zPlkH&9kICRaMi7=W^19@2&d$zjkc28wx^h>UTw0CA z(^!#rC_d+OOl+(TW^8JT(NJ{diC$V*Mo579sB=Rwi9@_9v88>(*~l6whGh=V%ATgU zk&M4`>sWB*jdC?>!pov&V(GED#evtlL+~}Uv#33mm!(#KHb2#4T0BgzyKZa?k5THAGTgjaag25V#UgY1V@(B3M{hXr_sg zbl_Bb@+8~RTd=xGDpl6$aBEUF|9k$1vSmfL9NrvWvDvvIyvD}6?)T1NI4^jM9>r~x zz5eKwCLSx^bMFyyt*-cbr2&Dn)$M*=!{;ri$C`*W{;CCtS&vl!T7lYriNdrf@<|^6rOE zQov{{P98R~v9t5yk7cuper6DapZp{!Q1)PK0zN^(2e&*tJ)6(X1;$s5C*Hny<&*WC ziGPS>`_YQhdt z=30LUBc~Lbm20`vSt~je&#YSaXs0NDpsj%mPfOBUre$2Ty{%4c+5YkI@Oafg<75gV zVq&3;!w=+tZ@}EJrNu4l)B`$hRyi8g3N<{fCUav-{;OtL_%for$D=v2RiB*8{yoK& zV^1z@HCt_$=evBi!D+reO!vS>_r7x^-q+d_k}n%+ZxFS8WE;MHCD-9b$#L@gGbDOn zI64A2&MCaqC3uR=kVdR%&C<5CJu&s)If{$^{C27cAIbQNX1F?ZX?Zv>@`B)+obX_C z-&--)>nb?+T1^q>m~dDq^LdHR;E)sdqTU?B36OHFR>ZvrSo(e{&U@1q41A=))l20= zfD-`VSK`PcNv{>-RGbfQTQM9e@X?hkyEzV(U~_;aWMpL8+S(A>K*F&=t1${Y_RlWG z#$7ef9hIAH!KljSfzpZ-9z7MV45-E(mpK!YOIguJR||OO0>_JZPu^D0Bfo{vU4Oed z^5Vyxk=VA02$3%jcDAR#jtu)PzEpd@l8k;32VFM5|3sl% z>)@{idTb8VB5yqm5N*;(T*e>l~@ zJ~7_;a$%=*`f3^)8fI1;ohQlgr8@$T z!2Ts%+cB^K?7w^e0a5h!)z;Pm{bhQ3dT#M)r3=xIkAx9J@V^0EIw?a45F7E_oI#)V z8^t=D7P=v&P7FL<6kv&njg1{Dg>X8e39eD~hp!u=43vC)eXB>RJnzcNAP@*=gSDPo z1)>Rx*XNoF*W@dBgg^0Y_IiK)(Y9VCNl9~-O?7WYW#3l1iWNrqdgxQ_jo=DxIykDT1;o@Q z=LuzRT`u7I%-LR^93O7vaYm16&Gs}GUM3T;pS0fIaIluNmg^m)wa64vr_r(ho;QB(N*OO3?1z?CdNM20sBo(+kq% zP~1L4PHs*H6a)>Zm+=65PK1)n6x$xR|9!S8o}>;S#ua=7lBLl#etu2VBoIe{SLL{X z$+8@8iUksZKD5{El`T+qP}`DY3=a!SB!>1HqiayXK=}m*s>;;Ml^rLe9~~P5I!qSS z!l0xMT+aPk2!D6o%gfK7m0-JcNgPEb;!$2>a92iXxkDK!(g2qn-%oIHaB%cf zTOP%^;!bWQzsJhLpU(1|RacZrN=!8M0vg+wxBai$$8BwGp}_wO_7c(WO-Mpg;j);l ziRL#h&{}_lBid?{PqL?aMVG+88|m9<7e0()VbgdoijtO-oBNiNu$9iN{;qCbC5<33fBy5=yBuPwU>WQ3tC+~ ze$_KH$KFs!&Wl(35RVDRIGREM9Dt`4Q&fL+m7XsLa$Zf&C&C4|#WGbp2PE=`TB zJ;?H^x2wt7q-RWcN9Sy>@*(KSgZ%ne10GEKboXj8> zofy`eW9ZA{_v~3n4b=ISB20ioA;aj>DmVXadAif0C0-0${!gTVLs=!WjC&}&+R#6e zRI@Vq+z^+lsdf&1NkQIaVQ;uQP>`2pG*VY*R?7^Q?75x(m5hoiBP>KH9m`Rvd0PreA`$Z0ixMh znkp4Mr7e>NrSTmgI`xh>0s(P0=d-Py9dyN}O6MQ3;+vsy%;f6TY7*M{VzN8&tK%!% z{{7Ktzx&ubdH`JnCi>;=mSzM4xEe;IB1UdLJ`M7b;bCW60vwXU7aO&)g#m>@WX<)4 z%Je?i(k>Iz)vMCt*p>h)7Vs0~mD?-Yc^A<#X`~`sid1eZJtn#5C@G;GSG1(nxc}+* zlP(5R^LKJH!d$RdTu~Vh0wZ|5z}x$QGBUbp-X?N^|=Kz5Ca^<&B|c38mZQaz{S zmSHS#Su_UyYgKc^+^X-o+oXlH4xw{EJj^ct^{nuvsYOTXOQP9M9!}y!U{=;vD@-&* zUA{ahL)r};2aYLlBRS2p+`om!y@&A~C?}QwatJ%XJ)k?yfCDI>p?8(OG9bJ@8mg}E z4*HPMYdqbNCc`+=q{vS>6YGR|89K#Ij^em4zBt}Ew#EW(B2_hHH(cE+-%`Lj)=pjw zY_};KFt_XJ2P=KheF^MSgp=Eo{Ae9+0s?~k$Nh)^fB&iO>^SOY0!ClLpuCs7d?>YX zQTV~kkWTu?a?(9|HkgqpcjT(rUI7(cN$T{XF!ugRonEHT>mQBZh`Me99PI6tKEwta zwrxuj$SXL%M=JVa4IFd?V&TJw4_-$}WlGU2y%rxoeso&?dc%IQB}1JZrrIB`L)++_ z5YirmSP)2^|9yVG!nl!A_Zkp8pe6L*5rZQlx`8|j1a`|Gz%czf$EAxqH8qtW6_~D` z4JP;K_x7?l>FDULCXEIwm-U(DI*-_uCD21G zd;w?P4s(mxkEi4uJt}mMOZgbd%<25&UXlv*ck*bI*e0=PWvN*XLvIbVZhoz=PrHZa zr{BE*kH&B;Bt*XqcoQJ82jjB*pv0oY1W1CaPM23RzwgUECp$I(3IWeLQP71-H4a3* zwMRI`87{PirWtof4c2`4OM;d5Oz9G|FUq6=Gs(>B%AHqrZ~Gs&kMY|Ke*)p<6EC1~ zt=r0jTxr^BUBPM<7YALzFQa%Og8h&4668ZEjTrK16AAy#=9_YvU*coPWb_ zO_HZ06%AJce_wbJkg z%h5?>QrK1`TQjs5hAUyYY#5yW#d6ZOzp-#E09PG(xo@Qo8PDhDfF?R{jFjL14e-|o zJ$KRM__pUHCYoY_7_3%grR%aRNAnsOkV*)gd2`IsU5~w+p--sPcI;VsOCg|TXhoYQ z2=z^OGyw`K&OG0Omnjx<-$ke%g4UcctMlQehM1#!*bnd-){?MHYb-c^wE24VVr%VR z6H0CS_tibj(Y?vw*8W~m^g<^8+FvV-T*cj!mSZ*vt*{_+$y7u{g#WTdTjFhK^X_oE zWMWd^Tx8j|0@QRO0)j71P0)-p?!FHWReiw7zTDPC@gmrwDsEVGvG9vsWM@}EVUHG2 zkd6$v56Ag_#uS#040Qz9S_jpKa?*XHPYJ~vhu*ks;@7V8B_J|0GH6}2{kjK+#sS)q z2aHXn#IdokB>tU{zqLM;VWuOE?ELvG;@amF?TNR^Msn1pDt9(Alw_h6sPfG_Wyssx z+Mt`lg?8Ts$>$r?5F~ZLbr4THI+VsTk3nlkFaX7ku`}OH*$}fl+oMcAmZ&mKyeB3Q zCyMMpka8fSS{E>r#vBWHSGwD*?QDND*d*QD-NTgtRrFo}o;~?kVGNH+`Oop8p`V|~6sSc! z`ZF=qP-%4&1!;@2`LO+ii)GRv^I`cO2(6j`(JJ0O0LaX|Kn3|;WKA`?&sx2ov ziAY^t9d64QfraX5`^R8Rdsd)*vgaBTN@46#W^2N&)ydXstI2oNh63gtS4*iAXSR4)YXd$Yb{(l7y6*b({4pgn+mQ`}%sZq> z%2P04aaeOz3!RbkVD2UH$~K112w>3Ve+14f#IP`)ar0&{*0*52NdX!&TJ$zdarqev z`lhF z8W5Xh$kTX1@3D5+Il78zYj3~9i|~LBqhA}IDk@=_GfdEvZ*Wt zSgP$Z{jUC%)^UUOT%)h)>wH)UDQ*cjpV5rm3fte|>le_hx}_=>=V@u#^V)Tu)mOI{ z7ZKYY~2el9K7oN~gMhxVtUW$ROMpaFkdAwp#US%ojVQ2m`qC#?)RM zj@gp!bfK_<3<7MXE0eLz;y%jN*XQ~rj1hdS)=Xq%B+#Pk zD2V>7Ftvzj8ZsNUlTQRimRDEfAPoV+HiAfNe=4eFHOoJe!`jjJO6^4C6!PlGSxMH{!PWf=ok8YjLt<;rRsANRSn-QWD;{0d>yf^()_vCKlZ{v4{e z>l*^$S-G&fXtb{)SWCNj7<=O%G^_sBoxKmv{SO~V5T^5@xj&BC0u^_XcnMky%^p=6 zfiDE+oqUQNq&_;RcIx1YhweD6^dg2Jw*o~d=x}baVrFL#dhy~lr_r;uq&yca`#;C< z^E>z%6hxzWdvYTn=NGgh&`IS&yKno1CD0}?!XVL>En)$3ECPECm<;o;BV@T;wvW zB{FP)Gp{j#29MIzh?c&&&^xzpn=Q>}fjw#=fJUE!!FoIBUj%=}>J9$&-t*RK0)uOw zJ8rr<@9#Z6EH&6>RysA&(Vn8PnOOvGMaGqzn|te#j_$2n+`XXvhhv3_nCGUv_8Y#p zOKTd>jo6lax+Ef^e>N{SmyYB1?b})i1bX?^K-!VrC6Q00$3WpGtKWy~?`f8t3Z&-S zk=n)9_rEU}nzhr*6#?cIS;SD2d=_;D$}-GV|9dVjJoj@X`6;A(L~X)M-q_(*_?ZP7 z+{wP8YLC<{M(qA@x?KK-`hF_}Z!M+AhZi}_~lkzHTQNpWWPZoh4svxnME z2T>IwWC0LtOuyAjy5Of`pIaNH!J=Ow$jO;I1YvqKhdy)* z)K>7qZuZeq<0y3d1)OE?Qnzwlnf>WO?BU z5ztZh97(ub9z;so{Y&I1)+(`2Q^QXTz0$S<2?Y9Kf9+)^3lgu;*#GhaH8hrkr2#Tc z#yqjL54&gyqJT&Gt_c44fEOhA%TEK^R}2B*o{EH~H2~hxf49~}jcq@-cvnIXFqQ?{ zmmyhcyR+p|5(fY8vN&E~+4l;$IkG5>KTdnG7bJkqA>3&RkPEy9v8chz*x0N92-!|o zR&37SK=>+EOF${s>w zQGPA??OSmcau$tv>~E_Wz|XKaK$7P48Bw&*V#D?Jxv9(kaL)0h)l(}=V*7c|o8Vw5 z41p4-0p#Avw-v9*s2CJpyR8lR4p=!oS{~O|QzM6y6Q%XZ+IkD3%>RG{g7c=rJrWwh zZ`KAK=ae-y8Ofoup}Pj6d=bPfwt@BTzsCO-`n zWW*e=MT7D1{frx?-)h6VaPIgz&S%hnx5Ow z8k0Ep6##UOB|4`bK_l*JVV_mIcA=if`k0s({#1CH$DYgC$L3G4KlZ@eZ|#L~xk3XK zhy_U7>+!RD0NCQXbjbiB;^Aw?4UtfE*-XPap5J#RiMwalq)!I?>Vtarhf}WlZa*b~ z#6g89zeuyZybLY8NX_)Pz1{9Z;|pt0LvA@%gCrul4S49Y}qS&Rtnj&vO{+Ej*DcIz1L-D@16BK zE~@+cyq?$d$M2uJ8`pVW=lPk(=Q!Sjxf3;w*7P7DUH2(9m5$HI(NbE(!C|kk??pxm z)#7~1xCM3|n`VPRUF!%$9cXk64A*p}s8FnS^m&Qy)$wXSHb3EaQ!_IQ6F`5vQK74+ zcU30~&bzSX`h-`wE_5a}!})495$Wu0(3dWymeK!2;^bkDqBrLTn^@s+{}?tyKCFAp zcO2E^FME4?NgC%@IOV(Y+T4qAEow5%`tZccDvyQa$`#ndVGOv=<{EWvV;=u5o{V$) z+OgfIjDecULp)pkq27;4yw*|=`BS0q&lD~bCU%0vc4(F$WT`}Z+1Mb`&3WmRz7Fm z*xI@+WB!b|H!)F&>~xnY9$kTy0I>73D}JXd&CbY=^xAGODz80}Vkm%UMN8Yu$lf!> z?&V%;asNT3_((mpX8QWc$)EhEOgRZ9eI&}E ztjxJ8qEBjczmTi+}5d%%7|~JzMQTT+R(@6p)qp%G%Qn3;O|Z@_TxF zz26Tlxh@sa@1OPSE@4d9e&g*po|smktbvpq=1&mVgVb_jr5;y+TC?+y6G7f>702IY29cQU2_Qd{HY%cNoIyqKk9Pmmoc=z4qf0>ulV=;^w7HC~YM{QOfT{5g(*D?1n8p`@7sgWDGgWIPb@e zgbcx-g&-5i{?~LAzba9az?247Xhv2R2^kbv13rAPEE}0fTA6p;UVQfN^UR0nM!p>g zW9|5t_|}#%BDEiLBd+Q|o+jlfIm-*3(Mq>tsL#p$txpLWLjkAmlW^Rl84#rK0eKQ4 zvQgK4Q)m=FHjsRPQ2`0pYtF8%(V(lRFqn~+HjZ%~ZBt9h@ZZdo2VOf{-L%z*C&0R5 z0qG6(5SQ;;%Om%HCIa^#%4wx>jvK6uj6PI&XziX_jFqQROKN|AfAUtc`G|FglspUp ze==?sYHF>y&LklD_P+M<@@i+zwA})1IQk$X1H;70*YdQ-@}H3NnRAduCIbm8)kh?+ zQ&Ur+aqcc^W~8R#T}tUn^MTc7eWT6{RJbNA;Xf&{j-uzXAR7z(_q+QrC|uwmnp)f} zedW{d-;Qe{l9FbQM&nz38hy2$ow94S(^hj2c@7`f2nysIeV_B-{W}m4<~o zkP@&lXA?#giy%X~z;?lKqF2=%NOwtp2VCQEf0@qnNfa%Jn~?S^wh)_AXqBdpye)mk zjaz!-tQMczzR9VED6V#-XWwMxcswH$yeo|cz zUXtfhIHC27aECYF8uEh|Q$P&o9TjW&#OW|+w|z_4lPdDX;SS1mkdh%%mi+VScy50! zT-Q2u;1r$EsK^{ANrlb-Jj&&M*Gizl^eqs>pZ&G|d}?;da>#@$JzbH3IG|o9e}Gd0 z1XTTWKIJ|-)njuSPVO>9yr9DMoU^|6e_mK0(xMMyu9KwtCMN_Xl;mFh9iR*i3;n%0 z+{iud&#mH%j~80zteb2JvEF}-3vZi=8trS&Ojc-&3ZFi3b?Zu>@^18A|6Rk_v=u^J zNse9T5w)wUbJLuE&mk|U$to``;e;r41Ra~Og3=)sG*_I>l%^B95oG@$R{ThN<$=yu zrolVp!e>ck5?L6_F8#*DzwrnkFAxW>o0u#NuZox+|B4I|<25qpN3~fpOu6k4iR{2i zO(D3lwAM5b-6%F-VbLbe!`imxzPKD0Zr2(e;S_5n<%D$mVfbzyo@zI&|wS-0^cq_}>{Ak;_7kB{~td9V*hEjTO;L)j3vByU!G zlw#*CYe@bc09ii`N*7V)6(8JvcN0!^PeN)^puG4~c>2zp<}I?w{;5-tl9LMPsW_!TtAHX$oU zydxgRAG0zCI&*M5-G^-bN4qGHka`yaDx6n!cv3#-pk%Rb3qoua5#gJe&iw?{EDGk9 zze;CfL2GR;T_L09CTNnTsTQ+nHLs=>gA!FJtHu%R^+m+3r1_A!x0LEhSIBe}GXNnc z8G(76q+O^)h=j1~H3hy7z9Z-rlW`bl#v!|G85C0uAPRv9Rn21iFT#ZN*Dl7~S?d|3iP z@xWgCMyjKnJR;3e><)u`s-~|pq&?Iy;Y@|D0yY@TbY%ZJy-ot2Db!*sdZIKS_agUIiH+T5;(|QU z<9Sm80!*k>26Ar+UyD>og0cwYMo8Y+T4FcTUZQQL^|}2+2Gxd7dWMKO>{rJ?acf%$LOf1z4i4~H@@WJqy1o4Os4Zemx|02PJPu>AnDGZH-2{$@K(EeA ztUvdK+o8=Fh!BTcP=Kt>S(zkJVk}D%W)&LB0muzYaF1+)@B|jkXUBbrwIc#y4_;}H zuZH6OG1T_oDIpF)3kOOVl4Ms}WBJOQ_tpXYC5wFN`^a?HQs7sQP)(HM{z7);FQ{kZ zzi?VA1dWeQ8z<#jZ{mfXjQR^*w?soqro&VXJY~0%=Otveeqs9iqUK!WU1ORbPzk^z zuOp39kC3m(SM}ilr47%(X$eGHF{HRUAFsmaP#C0CZW70}S_o|*sphkcFY<6}8-P-s z=v86n8lEmeJZf}3WS(LB%VntZ^TbC0hP>3(#S5B1t2Pmu_R@i(=+n}uheC)o=?vt- z!x{R^(`pQ6V?P_$K3#4_9~Yq|)%o4?HGW>ACn{mS1w+U{q{xahoDNds`s0r~c@a11 zNTm(CQ)NITH0ItTREhy70WZAisORzX!TT%alX8e4hD^oG!7tB(g>>N&g~$md5%>bM z7iiC1G}lZc=HC()joFp9jsF2@s2aX^9EYjg2d_bAsN?z%P$Fu0yhpr5*ZK+837r7G zFp5@`p3kZ@JHdnp#4LX2Ugj`*P0>p+snR_=9T!oTy)Ng@wx7R^Z<0LuZ6RyYncmEP zD(Gp&@sZO9LCx2-BZh_@xoA*7Jp?pO^X0SDvgUB}FTvq=(j^*OqIo&qbVu>=qSU+y z#+q3xrves3v}Dauxy#P?5r$EIKfWL@j#K#vVxGB70YPJzqI5bDtN<@7yAh6C+7}Yv zJsf_hPQhB+$4qL*SvPg}mvqm1I#M1%pN@=vtjyMN?SPgBYYvTsR>!vtorTiVh-M^k zgz1?P_70xq<;;@k4wxCuv@OOTWpMl)Z_eAc@$E=IvVZyR#uINMI_d8=mnbQl%db7> zBX}&Rz69ceJJdKHmx3%G@e#V20Dg_fofb3CBuO-M;`$@ICsiY0dh)t6zI-54`#~9oES+VL+m~9H+bs z)M&!uH#e;~)r_Po zFrtSIh?I;J%IdO>XW`2(=_b{(Zoa)!)YB=;`6~Tnh7b?MdYIiT_M#~F7bfqC!hOQj z&R}m@Dr>vyAWca44V=-69`zBM`gy3)JV@yv`UQ(_)&E9IK6JZgSmOm@$)!QvDix zZkJ1=H~!{LuD2bm9?Rabr2D??b%A3Qy9wf4B9;%aA77!;zWU7~mAfv}5%!Q$<<|v1H zjg2!}3?JQ<%3o+s++wN9JSWiw$42vF5iMzU&~z3Z<7M$wK%jt$#+a+UdEpPfp*!(< z5albA+5N<^$3a?D;{a%=>4F*J6R)Z<=Tp3UNj=t3)}fiL<@@QT&%Y5@TGbXWZ|)o^ z{)G06jpmxB!zaGu%T)YK{#B~D;i6$gn9{cjbLvj#V@(^2EGSQ9Eu>w!U~auwR^sLRzx2osjFk-a3Z=)8U6}c?0B- z>^54s31?m{nQG0r0V@myC-JK$NTb|ozb>bsU%9VcVfQKC!YL1fYzm4L8*6OG9W_cK z+KLkejUiGwket~{(AC$s;(d|yqw&umcm6ILJ0qJz`GF&6`DZvQm4oLZ!_~pUXtTEO z3l@kpq3e%+UK*TWxH7ieWaNJ_IXdDcF{2{(eZ_CIF?GD8!cK0+PzN$ETjJ{L3nbGi zp{PPyyjM@JId|R#c8*+uC(|3}uDrqHwn<-f2!iy7*m>~)Hf`eYz-#(Pgh5SN_iOmC zwajn+IyXgs~>R72HF@q=DFSl*P?)OA|3&|+) z4K;6%B+>N0qvO`z2FnGm-;z|`85I(?IrF94bmOgbvJ6GpU8Ik%R`9sj+$w(h74&7x zqTaZ5+{jvS)D|&S;AtuSVxu-wYq3yB{WBya1hlb)CeDwP*-ux(##eF&kL=aRLbXqS z-8Jv_(u*});kajBs=E2IO>xH5^<8ZnGntx!R6CD^@MO3#t9fmDxavc^psbrdMRaFt z+iaYV!dpz0w-g`>f>7u^)%N9bvG%K6a=)%|%|Wl?DPJFdU!!4Uls_hT#Pf2^jklyjSJp0suULkC78ZPvz?TA{se*YQ*09>M zEM{ir7Aa-Y0)vjTV%Fb`O-Y$_@m8H6Px>s-^75xR=|BfBpug{y5RPY;q3gK zrs87hku`i5uYG&pKJaDzmajBX2gK6^w?w769(t({=L1Tj`_4`s0XprMlLZu(b;}F9 zF>fzi%G2{^b=;hNq;s>h8}BLSGcqj&qgVb}Rcao)So{Ulc;wwr2ML@m2Uh7GsdAS| z3o0y7*bP6*_9)&9u;HrpnKDv9?P;*FS-K~D`|QRl>wYo1u)4TJyTM%PGWT zWrw`Y_!CK-XzST0?9&YQ1^Iq_JWezNa^RRIx0mb^fIfL zGo<6+zPnf?jO_5>M#m=oo-OI)XW&aG`=#MRLjAX$TX<7jcWwpImBi(&y*doyIVVPB zl}zv+w;f`?#L8Pt7ABh5OE~9Kon1{kzkJ|HRLD@QBONln3y5XcLkI&;cJPz8&Ai0T z9%Y><>4UtG5QoJeW6b(hNR*(^@98u*~=8FqwhRllwMuE(jF!L zEF>BHK4Cx-X{>(TJSn4mA`_y^{eN-sIzZcMEcZkEIWVQPCd!@xT>W>^2|^!SyBv)zxcfqMxv*|dJ!Gh{3YJGOX5l5_-;*9K&R$@S zYN^xC)nzZ;EET7fL!)!fd4<93;c1Ch??&$(T=R%m+Dz68zxL*^`j=?B*7EbLnwMX^ zcArWw5h}mhc-1A)Q6#BiI{z zKcwVEKQvgC>YCVp@NK!DFY@H?7YZXRJ$Mg%aR9_}s`D*l9<2qBatiwFCVTnGQ3o!9 zDg^2S`;2MwP2EFg#VGhq>z#jWs%&Gfs9OXdL}(_g@2Rsu=FF#lvPC}_?!r_3=iZ6P ziUca|-_QFNP_OJ$8X!-qe$wuEz|qI-c5flMN1eOcb!r7q;XM2jFYy5>0XE-vmkxqwj6q3iLuMf(njC~4=~^T%yL{#BedxOzPBZxLnNI#=myhr#S+ zPzNbh>)j#7dOu*2EC5S#n!NYF52?W*9Je^wKJFh}0u`7FHJ0E0ns;`WZadjsA6O}06NQq|7iDpw4|;m*#FJ#QnVr*e z$Zf5ach#iV&J`o2k9;QXF^I8jK$`KvX4_J2(v*LAq!%;nNT7TT8;P?%~#RDRN}d{4OdaVEGd7t^~k`pDC=MZ4dMy&oMD^ae&F57Ovkb zg4(96sQ>ldpE^OcfCQqbfpchxEQkKjzgbFohLX4J_GKTu0L^9_mAXnX7aPt>EE}xqhLy zz5O$esF+x&EUK40kJ0p{vuKJLcTGf3N}Q(AYW=^C z7bUm~8WT)DbUorB$MvZNoM2*Mh4tLW=;x5*6MGC4&a=HyE&62?(L+nW;Ci#-eOn8z zsMDF^2S}Rj(H+pbyX{lOGPbn5?C+4pC20+Aa)uUrz+?t!Z4BP5y>5#g6a4s#P>{lO> zHP}!SI4>utfXWsWHqNW z8f)0zgD?mLWZ+5lYV_~*ZR0%|&*7Xt2*5Iw3OqT_>O}1&)3G)CDO>J=fn*#bV`JbI zFmcFE$0A__K*9Yv05bo)+%ehT2Z4G1|12T`ru8p!=l`0&8xD{$j0lI4%yB;`mu3UR z3=+M->x7F=r!zmsRX;$tQ~;d1RE;H~1nb4<^Vr|QUiqZsoRr( z?a6!fjUHDND1T%iblU~N)1hmwTW+w@4KSnKHS*{Y;e{6Cf)V~GPS3@Kr3{gi1%~VB zB1+Yiz+zAnO7iT-q8Kx<=B^v+=_PGD?kuaDm@KYOH^b|HvQ;pBJgV>@U^o6IQ~W6~ zZ4Sz#b9X$e<8ff@h020iQr9Dp??&pf`}z8kYE}rHN^adiaWPJ$gm zh%62c&fw>g5+#yp%Sc=yZLxV%@T%g+pwMrPre_8JLbOjRVr>?-5|vT~wL?x5ZU@Se zp&p>^k1AxpffU_SdIb%QY#gjhm-J+=Nn&V&>KLk!IkADRNW!XC@;yop?IMZk=zR^> z;Qgt`xh^pSq4H}7;l0$$PN=Q$@RHSJmMjXM3M9!q2}`3#9*iQXw?HA;9mpKYA>k=7 zl6OaUw0w(Zhi?;h)R_W?I<;7e`YB}b7mA=ul)LO8CIXB0p%tS{A z%ilw|j=CdEc1t$)JDV}esbM>Ab7!Y5@~)Vb1X+WsO#Z-E90(J*y!`!NfH@2oD8pyV zike-92-{-R^Vo%oz*PK%k0lWW#cl9bX=uN&v<05$F7Xbu&|WFmd-+n# z&ro3fq2#`ynHj(3gpiTZJXF??bXS&j(wX+r{GB8tTl%bJ($X@&7H*w2Vqc)A8va?V ze@$yZ~OHa~DeyhS8rln067Jq|wV(Z{L zh27=SfcmGrG47*n8rJlORXld1%lj3s&mTWN_mUZ{BzVR}q@XcaP7jc-FlW>#fG1X> zGvhLwc&exj>%|!R2EVUo!Ko1jtVe1t{}Elj6VJF50H+59$AB>6HQ8TE5OjOVM0o&7 z$ElJlM70)wxMn4e2!-d*n;SRH%h&24^A4;flNlBs(O(Fdr0{ir`DM}Zpeg&|c5KNt zQ(*bU0PRd^D>(>HOpcGU0j&cb<_}>kiUSA+T9E4h?Vdl?F3WpSj90id1<*}5 z|0995Tm`nC&CZA&Sk5KgoQd9sb1knXXuLy3CQNP#=E~oPZw-u&Hervb*qykPlxP^q z6c!f7P==y#0HnAH6E&p&vg2-JUcMYIJp{V-oysUK7w8u;mciUB1#s&gP@Vwaf6_L; zXk;}Bi~s;AdK(y9BoGU+7eU${^j&X}rxw3{{rX9r!F@EzPdnozE=3KW)J{WJRYKUB zF$)2yniL!uc-@D}WiyCf`uTHifM@|w0ArzrAAAlkoEdxPK*b{2Re)=IO33iwweb^t*#uG6kaG=}Nb=~q9?BcP0A@9BsFsH$Y-xp9aso7-TYuNL!x?B7Y$@ykF!*6?u(0A1O2o`e`Bo5ecHH`8Ay~R zp$1)T`(rbwY6L3OU4vj7eh6+m;ueEf)Fm|cwtV%Vw+miyOfk{^SE zyCA<|Y&W>QJi?g5N<>UNw?p?yJh{DH+9n39wq~GY5^_R|01ya0GoV^>@@?c3S`3gF z0#qj(l`SDSUe)`Np~Q~&#iwh2gBWuX&>jd?six%ojK|<0c<~j zo=r1Z0IJeT6!E)qb&BYrPiCTUN8M%56K>^*{{M`gd7Ov)qxTs8 zA9S!k3^){`=pQj_9_KY<>;Cn@Qyfy|3T#KPEzRpUuwetY04Q*OIV-%O(c#yhs!PC# z2R{WX;a*r{d;Gov9Uj!^!BPeE4%9ty$P|^7pc$6zk@f!G_gr~Spph-_J6k{o8SriC z13-MF0%MtE_k3Sa`g_I)q}BN)ZaDG>Hv-jE9C5}BVFM^$)O96KPoS@0<5bkS2Fv8u zmIC@z$!|zPu0k!n2_>hFvb{qh${yGQF6Hlo7E(Ki7_mnHgf~P|Q&SV(f<>+5vr4|{ z5Z4|FZ&(GW8xK4H{%oF-ni_l`Fz+TSGXn7cekH8xA~ucmTcR%ksFSw~1)sEDc|}Fr zh2EB~F1fje4D)s7b$0SCbl|$2yj>;^kTpKF{DPpX$Uako1ll138!Y^|I5}szGi>L( zpH)nQw8VdHUd%;=hj+cFCm{>$b#D1UnNZHZv8f4CNq}VJ94s7z?$%+tdpA@A4|*)n zCq97fFYA)_qZ+}92jWXWKFR6;L=M;Ac>(+oOk8+CW?lv{kYaeP@;WSHpAA(o%1+{S z)nIzLWRcXuk{uFo=<|Ui7Vr1AHgn*SKfmou+7kuo9IZtNL=WD=$bYSdZAbcV06n>K zQGSf>kM06$iH?!+gd25w0x66?hk+}S3UV@Z6jzZ$0NhV@Ft7Wv1SG#isMX05%vFt-wH$Yz;dtH~87=ZH(BB`cVl~6cor0EDjZ~1U;n;axy22%+#oSQ4w}1 zz21rbKmB70qeNVm35aTUeSCa^Cuj6%vhyNilSE=&ur1Ay!~d_ z3$==!`JRQrxl0gSSlKGd$`Wj$pYj`ux7EzoWj5E=fSKoO#VrLU3SjsY(uykPoB?a7 zQyNNUOkb2U5vn`{EF|&L0H8BOi6=KSh$ImLx5@_=`2=Kp1qzjlI#`X&ZIOUW;trBi zVq1a{OVyw?g?xu>1t^ z(Nq_((-XZ7ok*g$0?9&KE+815ioN9bM4d_F-g9tsWUMO3g_B7|r}6dP;Q)YUcJx1fjwALc!;Z@(tHflR)zfZC2o z4i66@Z2z5@GgQog6zi5&@y7@#hwxf;K-d7QQQoW!@wo~1P2{8VW3aG1A<0m zCtzB`Y5`okK1Gxzu;NZ_I@U-JH~0}NNA`>}qlck*!1u`fEB%~WHIUY5P?n2{*8Il@ zuapZ=Y;@>R)R}S60nzxh<0$iP=^?FKi%k(b;DW_cnZZnJuE9)B`DbHe{9m?TZ|s68 zz#N;-uqX(?diM=p8UPq~lQ@8+>#zvY^HTphk539am+!I-2YY&7b@gN12HBP@wNlV_ zcn#zitKPGJzMCfr`Q!oyM&)`Fqt&vYfqt`!{Q6>(^=!n>$Rw>6ZRwKY^t@er@8#D6>AK-5b_R?;~gSiDnqW4L40f?=BHBfXPfE}TJ z5AH1WWQ05WJP=GmE=Ol35}5w&n5g{5bJA&9;H`H$d4UYcs}H!e^&J@0bas;}nSVn` z=#i4amp;CYf9yQ(u5E3Ft^#mO<{l;{W**A2X&QwCx0&nIgSbZ6Nph)+(@z5ru50Ja zbXSLP-eX(Y+>Q4aL>TFy`4;zX5HLQWl4@$vOv1v#3>Xa>%YdH(E4CPizcD4i$X~<$ zr!Ri${AR0U4!h4|iknf`knPAeV?`V_rqyZ(J-IrRVfLkGjWpTP8w zX_VSpqoK5P;JZNx8bkccHK~2zSkRQ^?d=WdDRoAUh;z3$Tu-j_5dsN8i6Z)f z{w9k)bvrP|9TZSA?$6Nz02m@qxy;rSXf<2C{J&5n065HpmQ#mgFqytpYv=<3g?mUO z62eMAdIf_I+2)rX;pJR7-$@olGDLyTHt*^tz}>o0yo=MpSri%(LBEm}!(%CUH+X{p zMkZKBrJ8`K)?-LQfheZ&Q$q=$I-9kQnBH`Q+EO#}Jw)pBEfg~@22WBx02dNg!QwP- z>X*#mWs0)>0xms(Gr}=GGqYU1DeN0~lO!v~=(mCKROFAW3(y~@ui1*C%MCncXnWYx zqSKb<)B-`^$y6M!KyIe~<%q3m0977F%ioRam&*yo_s=~2M{|Rt-QPC?p|I1542}vE zs^*|85R!PMyCs+!1^tWvj~~U`2Lx@IM*}+VB9gHORUGL{ad6KMDy=Q3S+d^f3^>C^IoI z$l3x%uzhq?qvG@bz6Yf3Xu79*xRZ1WRrJ`v-1e%miHS25vW<<5Qj5v{C^?)ibGA+> zBOJlZC+c9*BT52>Zdtw%aq8;o(v3ev0QY2%R7^qw2(M@jwe9u)j%~pJ*c43#H7~jt zNCU!l3-VNYpA!;(6lGS$JLXGu6>JySSa6iJ2T1Y-;s zCN(E?#L9tO3rsscvyFb8fQXh~fLU+qL$M@cq|N^gv!lBLN4tsd1|b&`+6qxVoWWi^ zf1W}vt**{|>_LWfLa`-Jr_WF|1m&$_()S<=xqR`WG#8}8z{#@HA^om`f`Y8M)uUVu z)Q$hPJ__juu_styZ4~wmPEqN}44l7k;aMDn@O1HVs;XeEK}Jr_$6HZ^+ol3Vm2Nk2SV zwmSSYRadDgv%IRut2kic-*z}`t{$^#Q{j>Cq7g8+d6Y9BF?ltENw94w9#YTl>)i5ip+XfPQRQXbV9}&uGjQ=R=ZeDhOzbi02)Ot*utlGlL@Y`6 zDmHqqk}u+U0x}1N7Oa=3;`0OV+JpgkqXJcIw_>^c58sf-?EMV3zM42Ln#2gmA)%cy!OswBxy z7b^NLuu)g=Nz*ht8LnsbPU8GlXkkqKq*kA2wN|0Z+Job9j+A~NM!|B6D%_$s>VCwt z+F>kK$*l93NWrVAn`FC><5>+Qf4qyj>{rW1USujJE&T5*TqgkCUJ{=R#9AYD9Vw$+>)qlQy`qr$>Q_B$xpx9T64#zcORxOUuffoSj1$U^HCp zFe?t|9x5L0i*th+dAcp8jPLAIQBuBmS(Mqg0IPv6JH3WHLnvsK8x^MWTfqYTd(S3+ zfUYp*fgCb|^anF)48eA|#91_y*`j|5-MxC;;sK>v%A97q(PH|IwfJ zIsaV+S)ac$2q(Mqi4MtWJOU?ec&L*N@03-B+OA=;LO4AX!4%$XRl2yb_kdC=Q*mfz z=a^&id1(!xHAa@TbAe9tuVyrJC#nUC%nYF*CpEd&51bFbx}9^9yj_}ix)-);wb^1M z!S4gCnkq*@NAEP+b>RVs&6iKkxkF1J*WWc)a8RsyM+vf{qO`#<(%_j-m+gQ;B__^m z5-ebDun1Tk01P;@`iy^PD=~?Fd=@n)rJ+U{s*Ue&9QW0;yz0GZE7kyZN#(HpP= zVI|2v!%pm=OS?~w3zj!bI=*}Y0(}cxupg-22>OnG1`W@1JT3|ajM7wrHPqmVGlS^u{@hb{)6c&gLUc zJgj@GmljO#kdnIA85Npwk=6n2Ej7%rgTv8x^na#W0aH&%El{*o$t|rKF@> zUReP@B9IM2`|fnIE*sJjVX@VL!38}{@H4hW>wB9Nxk|&&ORjm>MsHZ(gRt;#TgHRQ z2q_~G))zc%bg+czW+C(v${#S>zmP)+ZW14maR%dj(u+3 zFfShJcv3tBPI1tb8g4&|yXH_}m(#t$Fhel|QGTGa;_ra{p&HFMTTohrX6FJYK`5P& zlMo%}4bcz6iPEz4UT4zn-om1=q>-1S7t;EL>>n1kinu?ZPG}sLBE4wF zUR%C#GRG?saav$kp?suVQen>f)Jg(@4)rHr9QX%i$TgWvC(05YtZrC5#7yFoo(?y& z1W?JzJs+C~0EcYBwyb%YJyaBUdUb3+`#+=DS~v)o2>6o!9~80q7XAFjZk;E0+&B{!ql4+x4QEWJ{x&{&41=n@gr_ zy!Z0rr&mTjXNO&1RI=gF(smyX@cHa!GrQ)%X-g%u_fFP`SwLJCu=$QM#w~bg69V)$ zb@{3YZ#U;?!3~vD^5lWvmbj8tyypyVE^f%cg8@ebSNG7QLU!Nenmrggg^GRY3g%Z@%41*~0Wsk?Gl#stLvi4wxu1gxU)~=okUVE3T*(UG}t|dD%1}GudI5w^g6}7F;-i z4(XpIney1FLPiMsh#VJY+0zYYeqj6|UTCDrH%JsKF)@+&bQsi#sbutAtW?R=PAO1f z-o`g}+9!k%0>C$oFbm<^YM3_m)MyCe%|1J7HiK`|h!kxwd&JL~jlDzHP|-q_OtVU+ zNTh*d;*(>m-{)l9k&CwX!_8Y#&hwCzl9kW|ocY{P8 z?ByygPEWs2U6dz^lq(w%w%{Q@fA&p}5#lDry#cf0#}7!c;f%ek?ngw7b-|Z>%8FNd zvI9d%pFINnu^`+@e2Z4tVL;Sve20|MWmSzK^anivIcbus(WyJu?1hF+xGb-e`Jqoh zBOF+Qv(uf3pNFn)i6!fM50GC0Y%+CDHaCZcc7Dr|jsnsYJO=|NVXNR2L=n-w$0(ugXO=w|fX8_mNfq6m2U0oiaqSo;fU=3C_MBvBRXoZn2NZTn`j zL|0UD@p0c6WuOa3{hSP)C4g=i@Uz&b^Ci)ARNAG$S^ez+{IfC2SY<1STF1#pcU38cXIF{XB=3dCa zc5u`K+HpYrv;N@=h{gf>vE;5fkx3IAvs6Lui{B!%UTbtqv`1jSmAX#xu5zLrm2Uq5cpQB zZcFQk{2c$|L;E(^^qkMAMvt^F4ne7!o_SD$SP z&(L~e`@=AZRBjlY{X%^}C|~c%=A6+3`&yem*+R4qp+!ynaljs~^i1-vWHvpln^hh+J5<>tXvz{pkh3%my6kGlZ+#VXKV`B zBQ8g3-l}IF@ozTvTyyPqvQ$i-lYXzp@w%Qm_8gm@CGwL!4VLS9;CrnPZD#Fw#Xc2gCPC3l_kB2nshILek>O6fI|!!-FLn9e`;s}- z19W7YqfKQ&*)8DFeiCESTkLQ{Yap1$50Fg~Kfgw}4`jz-nZ=lz=Zm-H+hlUMqxFF! zYbNI=JtbCgTwH||B4)-Mww_4FtJ6l-?A}17;Ud`{cg zs(zM9O;BxowF=m%tP9eqy4&e|YB7sJ;1eCZ3J5H+$@ItFUe*%S)w;mvi1>wHSk1B* z!1$cViE&$zZhQOg^|A&rjPQ9?GSW9id)y(^<{Kg8fbIDa$Aol;q;E5;$_&K~R?p1u zo5{C+eHcj-7+ROpL;<< zGgkwPq8=V4d@nG1$f3Rg`FnSBE|^f_zK4iVzTn_5$MhV@bJqhLqSbNLge7meixw#q@vf>7y? z0ug*drfLD22l&=+;UZxUmLgvu%k<4Aq0fGzU zZ-rwCwO%JG`7#RKt;S(~SzFm}T-C;Wm#uQ%_z9j zxo(8K`k3#vF+5DZ{^k3v!k1SgNU&eAPhb(W7rrJC`S8K)DU!%RB`DXZQ@foxkuULP zqQ3c*>3S(4{gbBd3ji)Fq%Tzapc}iR*daT!o@~WE@|2rXKy*@TXOVI0$IQ3nK+XBW z2s?(yBx0`nBW`Z}%70$}ELzmsHKS!b^xX&#jEjERQ=dH=uPc9HuaxFuJI|+pF;nPo zv^qRrX=)l>cW4-JmEI|z_b|irq6odE&G6I5jtor18<@B?Mmo_uUn6`SwksoB(jL7H znTwQOq2zZq!0R)VWdD6dkgOHY?Zrjr90kOk9q$iO1b(v4JQBW6;=(OR=PT2}J*=92 z@2W%UOmqa)Z(e`j^evNoW~pzCW93Osxzby=%Qy-eigS$url}2vxy6FH#TU>TbwMHt zl<0PtRr5On5k2xhF=Ru;udMe`72uH3Q1u=ZXq(?9Hm$Od%pe_{cRRAo>Q_Cz^mWda z$|EYbuRD@(pYNd5pF9v1N#OovATmzd+{bprN+E8=qg29Ow-9|s<;uG~{yvy7b^I-o$qK?4bI1Yw~gfo##z9Ui(C8{4}hwFdX zYOO;C{#u#w6t}@Zx^7fHJT6&yRMGdKt)JUs?}=RG)!!SQSlC^6@gMftbZc^+H-5c+ zrSr&Fab>nYN;y*NZtH9Gh03k33?YgNa%PB=Cn7=XbeSsOc$%3;w`3asS-;nM0QWWl zn+=Lp{WZxRcOE-;mym?v$+2qlV^f2 zD<*a@`aE8vT4Wf3OTOY^{dwEMPX!73McJzP`Z$xHj;=pvu39id6Cu816L!Y#gDd(s zn^bCy>Pr`zP5X|4U7qRIj`7u80DnPaBq2W_5<&lrM6i(S)3%{c--_F{j1Ww*S6hs( z5p7~_)b!@XM_2Q4f631Uu6(eFSkB9wf9X)R@NO2v0i%RP*kIhWYKhOe<5O6! zI9b7~2iqhPl%8llcWG{3<2Mp{&6{*NTDs(1)UAa)fY)04y>@62yR@R5{QAM&AD_p; zE@PXczAszjxifdZMe{es^;>oYCbO*<{Pd3TOk+r!FN}{>)TQ5aDp@PIbot_CK{a_o zfOQXkx!|J$x~UebG#`Kc?%URZGIzC$fYpc+@!OXsF&fsM|M#j<1(d?WeyUAX*%}6#oX*BV!eg><^Y~?=EI}! zL6T#FtCLw50y#L}){)3pvnGBS?*&UhGG=Ltr)M8O9+M3l1so{zlN{PyoSQNeU3l_* z`B!>NVy&n6r??SwP=TLBBR5FxY0tdp^qT@ylqY<3%0;jE8E{S%x{u%O>mErnJlM#7 z^`|XdjB}nm(F|dlVs0DZx4rv?VZWY3+ui^PmQ2AC!%&dRgZS>IRWk)O-b_z`EGI%*;yk;*LGxJ zEzP0UEoOq3lTR(LWgUMakqz8pwU1m7u#qTFCu1HiT=chD9_D?&q+AjcLgb>`J978! z%6GNn>1bgOOXOi}v< zG_c|Cf9C)}gDVG`-4DALz4VDAqGmZP0S5e|8n5)D?&T&u>#EBnewqdN zwfd#`fxgBZBvebdS*>qfP(H4?&HI+Hy#BV|)=Hb%)OQcPQhbTq1T0Ug4ZjO!@Q+7k zy9p+9ewf=@v5p@habi3P1wK&-T`yIZ^(hc&s%m&gSWVpMZNB4i9e3|W_}dLA+VFn} zi7;!UEpNO}>Ez@KIw&GnYm#K?yvqwf(Y-0nD3(*$HTcmja@u<&RxRWN4%}@tS66aAcw^e%8#k$c3HL5N^htn@!sdtr*|_5QoJ9z?9-&sBXSS~?`0yb&c> zO+8R_f#GI}?{|s5cj>?OcGq;cU2|N5NO&piiiDWE&CsIn*?5QWw=7M)p0`z5%#=p$ z##$uy0XJW!S%^d;%k0cWgs?8L6F=h*OsLJeuvj4#$L}`@nk>6|MQ8nETA$_XciCHK zQ{ANTl=<=7B@dI5l#E#pjj?X;T56mDvB!3EkPHSOS8IzJ%?0I$xqZ(Q$;a}(N>;5| zbt}3imtG1a3UtdK;p_#J0;lyz^^D$pET9t};fXKc<@ovK zp_1R)dwtP->u@}O!b_G#vb|kSBI~a?)-|j9zjn&+ei@x}m;L(|b@tvd^qGuwFLIqQ zP3anaAF*n$XsnfFT`+0P>sTua{CtpyoswD)ytRGrV+E7AM0edsWLk!$x#1Se>7#-fe@#m8>$#65 zMj4LcmA+k5ePT)s0DF+X;f5_cazGZ}V-`Ke_eC|veRVpQX4dBu!u@N?_{G_5iGH%i z=q4wCq10P|szmCGr*qS_*P6okvYMkhKRjh5UxnhwNC42k@KKK5MBqU+IDi;MMKMOs=`x$Wg z7n;_4_1%ga;>ljEIxR;P5j(bz?t94VPY+s46@8g?+s?hVJU?$VA7Cp+wSVnvLTq;| zzZ#SWb1MBk3svv64-fTY7JXOgP848{_{!_JKw&I76drFzR%Rp-Y${<&(xOwGnWWHV zN6oBEdIg|bcfs2MfiRu?fZJ(l**?u=#9BbYV^`)tUL0B2H?>QiV`Uo2mpP?qcU3~K zm$H=Ni|YJcxvm18@Ibc5id1&N-{^<3FUsS#kL1if`II$ewz{|b4Yeb~7M$huB>D?_SBxJC7i_T>-&WqKj5Lc#{_0q{I7G(- zw(rGlijIYb`PWA9mmOBY#Ba__=F0xQ>SMmRwn~zjwYA*#ImRNqFU(3QnV~~bnek>1 zqQq6!t)qHh6nMV6(#btswqJ0Nkhdb}h@EvwZL!NK^LEUO$v3vLLKYwykKqLECq;*zazI>VK-Lq%QqF1o9v-|t|9|LWA z1@5u^1iaw=#krTj^{~J}rKeAqGCBgMU<2|uya5i*&#t?D|Gv2h$beV8VF!RGX`Vah zcUoLePp=1f3NKK9^U`n6&)b*1mROp|z;n2OHP$Edprg94iiwl!3NI1X^z9|@9A#`; z1G*>fDiN}2Tg)!7$@}D^ggp*t-)U`Idj7DvisXOQ_wKWng@&d^im^BbisUr`9rbbb zJ+{9$pPW4NY@e3=yyL&uZ#Vz?`T2{y<#&PCPSrjCu9xh+!7(0`z82p*lT-NZPH+G0 z*|()%zfX4up3rmktQ=?SDTDefWQz#bi?| zG&L0%8+*6a0rM+DFUKRkbz6Xs@2i{jJqrI|K0ZG9Khln==&Mi7i)tiJjnDe+SFue9 zqakvmp-#(zjH=(d`7GJutm)N0aqBBQXOacPBL&TkjsIL{K^al!zpx9@+HA%5uXU~* z{=NFD#rV8TuEtj?Y*!JZ_+TmIRb}LfMg)9}w1buJKMI3a2?_VtVWH4uk3QokS#T#f z@g?^4;l%o}y`eV_jT+l>`@dJBAZxJ0K(^Ew-4QwS)#FDknSaC;3RYCO-IWJA4yrgV@;K7l`p&`ktg`6 z@nw;Zf|*dyidR-v$n-r6yte+(h&ay-l^5=qikB+$^6@nx9zI3YU{m1mh6Z!UyJnne zx8vX{$Ao;^t?C5-B+hG@Au~1 zO}h~T&n(*HabKAf8ue7AxpL)-dX}2Jf&wE7gA#TJ`)b7{Z*yQA-Rc@!f^^uc@``5YM63Ztm{S$@iiFp2;kuiiyNMp~EU%u<@&Gegb8Y$O9 zvK#D~t_Oq3INCpxzUQJ}KwW)(CaN)liIJF+GRr$hx3a{z<(1FQYG2R&@=v}vIHykC zY3iR4Rl_>sDmODTbAX(sWsb0=bwGf$v~&jDS!#7?kl$%-rkIpHfny!RQ_78woN}N_h0ggzBlj5;(hR?tfRNYYLN7jR@wbAc8#2G zX*bAL3lBCs}FL9hCj<4CPuustZ?6- zV^u24{}Roi$;Hep?KNZXJA8k(J=wR4!DLI_%3QwCxMil7-Qh0@Ez*?|UI{yyk5+qG zN!9ijnykA22@Kg*_ou1vFUGF?Nl8K?;I?Gs(V(P`R>=ch<8W&{{``Ma$0K6@U)Aw| z?H73{N=-Z7fXwFUR)$hv>3q9I>Rq_|JHN(So6hq4P{I9%zt36@#b5GT?a>5NVS4Ui z;rsWN$>yumJ*ujz#n%M!5u&2ondz@Lr_vMEoVNbVDvK`G)$Hy5%a~uS3ECx={kUXY z8DZ+fUCQFQSd^l!wmn(uswyh{%5ag4(eK_|qrXBtwlR@did6Bp>L-o>&B>%Iap$0? z|43Vt)wVSLwGym>^8LSrSvHL}2bExCWb7_*m{2035VDTnur;J-WQ3fy!B#PL zzP(9R6}z(HDDtt848@}O^0M7eX^}{e`KmQq&xKEQb#>atI%;a&cW_2=kJ-j3B6lug zecjb*dv|M|u`}Z~Q&sHPm=O`1W?uZDfuM7${rDF%Gc&9QI^Lc(;3)R3OkT#XwnT2m zKlk`Y@v(`_I^Ot=G{cIS+vt3@lf}Nixa6>V)tirtm)AbncOL6V`$-~n6{;4Kr@=DF zq^hPXx$_AcNlUYCXzYDBpG;EV!H3n1hC5p`f z_LGI$ny(>?0O<#(e{9Blb~hSYdnBc#Y(A`AzK_IPO>tZP<@}WaterlA^JM!!);R3j zJt8XlH=&EDh8S-B-Jx=az7i+2WuNRwndSrO2D>@dZPFTlg>k8gHSwRP5NVt3E!3VJ zq`_*r?{OO>gNlkOZoaB?bM<$28<^;mn3%KGpNoAgdvjuEHH(+8{8j1?_m{nEKYzw1 zpf=r7pM53i=vb8SF*7XpoBLE+jM(~jOs9A;uZjm>U+7ji7T~7ee8vCYfBzMXBG>hg z*tHxg!etHC&u4FE&1zL&Qu6VR#0$G4ccCNySxNoq)c>J`XcjOX`o|-7M1ZgS`x#Be zP&nrrlEPu}r_812QvL)He`#9`{OYII9}F-LD!0_n${pz(G_cdCa5U47{nuSt(n;OA z)m~)Q#oxwVvu#*e4sI*OFA}1#N@uIVsDDmPzaC0`H%%dWXh>(3;;MU**YoJ;G{4J6 zjp396=Kqkp7<}viO3lgg+dB#A>WyDiRnfYY&fu!^!IA#mfLqmheQ7<9Spg^J3l{<# zAD+N0x(19Tz{fA9$@%BBS#V|NA|7GlWTAl5jD|6^Mc3{(gu*Rzv!4I6U9kB9aymNk z!YV2%(y8j%8q=e@J6lXlOg%$NNCC@lm%n{lJ9*>_pPBxd>1QOSqstHN?p7l*YfpN# zX|Tosp#jK`g9k+n-Kh2vTR5_0f=B}ch6U9dz>Z0$KJ0qGHtOTk^!$RkH6QA&V(ikM znCQJDzf|@{yUM*d0FRPijt1u}?fD4WtL_=KA);$Bu0qGIxZ6W~d%-tgWvWY-X0X(T z`}^DJjN2)+UMn5fWb2=iTta~>c_3bTiuu^Y&(+LHB)vF2rSfp>~ zNB_(Y4o)IOVk!Gqh@M*93&9>JU z!TTdz<{%6_@_-lc&Z-~TDTwk+5fAsJ`dF6H(clr|_T32j07QoSH~9DSHDt1pQM{Gi zJET^b?GG@&MYc+>Ma#{5*awH4))x%^-lp3_aK01*3_9=iA2Isg{SXmo>bcdI27cw< zYANYs5EC1Diap51on6Ev8~TrICxE%^C^3W3>Bh2=iHZ1fGoPKE-Sp`53pXtLeK#$K zD~cgtl#q}p2@Lv6<1mO$x-(T}Y50trPoF+5cjJbt_zZQchD|xEvxXBRug!?)-=&GV zU&vEQQ$PnQvWA9l0-6&4rrK*o^4lnoP7N{Nd8g*2tZ4mzLO9m3CXm`_M6d z7Q`Of1^q*WPTPM5ZT=+Fyfb6?Hrt&fYSQ*vU7eQzl7@q*e!>q7uT}v*1+O%X;E7f- zy#M~|kPHY6{0|SWXzBhow<+St$%=qQ)vzomD43R0QYtxXb^6^1xD?e_f>!1!^51?! z?r<2-EO&!?=6pkk(FT#yGqtcEvqzTWQnt3X?(Un#>Dee`2w2P769-{8lXLW^5lhvE zoV$gbC<1Ckk{DQ@-rrV(CAsiV&{@gLdFC4r*bVfm4q8j`6el?Xd9u6T5oW%1r-qkEq~ksmuIG$$-J9$5=+xXo=nrrcE|irY>m}y&KPh zn7^oR7_H`fvfV(Os}H_JnSu2DDxlFRk=3_GvV?`(CH;Vu8U%fUt8K$3Sx| z-`@7Ja@9ztYPu!MQ1JsD@pXeXy(Yvl3*^f(&rlK^#Fl6?3fnzMtqx*2bjX?X<8NGd z^bAO?(6QW6nspG@t<$xYf#GhbuXmas@u~|TN>@%~IVNG=sh9j_Dv>NV3FyQunS5YN zI=NcK5Q-d8tWuTN8ca`#KmUabGB=_iWEdMv5Vs#|94NNXuW$?u#>B@#r)YBl!N0pR zyL8>myEBMLNYquHb1^c0-u*oN&MSxUd3ZQb|0yO~a9yO46dZ9p_mMie@t*Q7AljF_ zEd{*6Da~-_ghsL|4{Yc-I`UKC91vibjl$)Ar+{hu_dHPurfEf-b4dA0SVo_RZKf4rS0H5S!dk3*}3*_~i|>9yw(XbX;B6L(=WeUR}BMiK6eFLG7NyLKXOVG?5g{ zk^L67&YjMyKc;luW{9$?&YGxCXoT+2Qr&!0U)!1>lKahaI$s!_oz-3spCF-t`uGSlNEzARsLxUI?%ic4NQV0T85kIj zsgfSNS6sa)q+KUgq$*sQZ|;^}f8E+T@2aR5gjwjY?H?7X#N=)5Gk50h8NGnLrGDe; zC1cBI^J~{0;#1sC^us078~%OF${Dpw)s~L&xg(IbWNkwH!NSiUm5_BLj+#RW2)j4n zHlwvlr)l+|jab{;Vs{Ss`lEte!Q9Z%wXTyo$c)p@TaQEV`UPQ;6j?bl2Y%~I+%<0%>37{ zUxN_#r2b>SMCru6yRWQS^yWl(hS6RHZ$ARY_l9*Dn0B2SZ(V5>RaKMbmj(rQws&^^ z+6TTcrSY|eh09`83F1E9psxz9AG1hId*!E3(>Q?36F zrf0wiq}IWSHcug%9l`;hh3}6LEw8U?*BBP|Hy)cHzFJg6m_QB7%~#CWYjeY0^|5>= z&u}*hOTdE#1_u5tvh3&Gd6u8geWa%mcE$lAAwAojwN^5NO8PE@WbfaLw;&6XE3u~_UR$`@X*9JD6LoMU`keNe*o!??5)`5;SmU%**2Zw+@Y>ol=B`tIF3pDT+GNsj!(E(a7T`S~3m zc|iR`X6#$ReJx9f1q{QZqEdsz#KcryJ$dp3YEQJ{-tlY@(i~WKldQ5b#HEt|&as@= z#lypkB8E~y7NiuKc{*%m0Z%-R?QJVFfq#nk&L(KZrImO!zPY-NH?qQD1~(eMh;t-YUP4ec-BNlL*oU$ z>EXkN_*8;_9goTLnVA`gqAi+9EVFv@bO*_;fas-nICHg2Nz;j#u1B$jdOtrhr zNsS>0Q&+1;!TF+nJ9i|<%1KMGYQ+|PXG(sa)NNU-ji|r&*RoU!D_!`tMU?71FsO z5O#$?+vCqn$H*&DxA%ojLvLa*wK-n|2jd+rfeF>@0JQ`V_4@jH52;t=90?H;p z&7g?KaQygIOOSUPNFK>82Z}Nw{%dX0N`vsr73AREMl(w9meKE&XWQ&8xX8BU8NuJTy3o@`HMlsu+NeE*x2l;UrX zA3v`9-W17NF*!bd;R59qdn0@Mr9_JKMApCS6OD+7(7NX^(MsYAG0wDM43}Pm+MOqiPBFKu>DLX81J`32{& z5k5vnAXL{$Tmk}8--a-{--dg;JN15cDC7lZW;rq1tjC8|dr1E~NYkEkTVKAu%oxy9 z>)xYBk7PzY7S`5ceoD*8Y;0^?AU9o@ns7S(WLJcP1A^!y&D^V$4Wl)CH1+k2q3w^) zSh_mz?mars3UR`k2Q4k_rrYq3E}!34AOGQV@&Rqn`_2xQTD+j3ixZ{}eZGhD&vLk3 z@2Z;!lC%CiOC{Lc)z#&?CkGaBMKr!Fi6nz!?ME#Ht4GSMNJ2{5TWHb-y5B$RobaU` z3NJT>P!Lz|uBU(}SE>8pw!D(|jCUhPBfGV`FXqlayk8C1F*Th8gHT&r3sE}9^j6&1 z`@+Js0CZ?k?jWoGG9WCh25t>{vU30L9(IhOX#O{vGetl{wkJoZ-goq7S`(A z@D|j5GBY#5N_4Uj%#YT@bSlR3rhz%>=;(OmC4936GHvHdNGK2agouP>CyGOJ4s_yY zs)xNswz@(Q)CQ+}vMn~`t08Qj7R>{X9$|;h1;Y0i8wtrwI&9w1_WQ>ZLG{n0qL{fp zxUVXFZ8I(m?3x>{1nJ85a5aLC!%_5(MJMfHzqYmrp*>t-0 z+gTqVEjSE;-}1JW^wGybm4pr1Bof)0C|+Yw`_yG_h)n z8yo5j%H>Aw3HtqN@SZku>aGxv>z|oeF6X(SyU!fJxbtol{i!npxv6+FXn(;(r_yzxMWI3PRDLH-i^V|k(t z>MYdILm&rjHH)+wD!Z)%cFFfeeC-cH)`amSeF}svJq7UTU_UI2fBxh_A%c98h7r7P zAIEsz6!c7NMypBE+uLu2lz`kxn#}a(>nkHWY@zXnroa5$XiYwM?6j$8Q%qbO7^Fck zC4pUV{Uy>HAPe+Gju9pRb0k=Q~nlC1xKycmN*V5*bYK&jlt^ooNE*-ItX{ z&p_J!oG<5m3Dgy6YA;|5%ho{kn2n9?pR-f%78(o7s^cjoidhxh0jE>%2ma>wpE%Ea z&=XThtTw@7(&U=x80`ILw z5?=7dKsmMxz88I_JrW%gyc??KdMFeV`_^YEd}=Ev*0x`d_7s%-ux<3$$d0yt*<#Gi z&FvX-J!Q(d()ZIto2lrp58^|lYEJ;i!|fW&aDL3zlP_<0GGC5WTo3mr8`jA06BiR( zf1jSt8YtaqT{^#^5^XYi_Jk-Im|$pTLpOHq=9J?r9@5jNNcUIVzdqAA9tMJ;l=Vi36W}x_1qk z`~%(WhmQG>uWQxF#Kgpow=$V^sWvAm9f5zFOEFhW_8m^|UEIB$ovYCb9h#&;7brs* zQQ(-sm_R>!P6Ei)VT$Kp>nKPYc7a%4Fpgy85+iZo`7>C$YT2N*eOkHK$rfF8vU`?D zrT=*z%CdA&j@hu_4mE*)zh&#hGP5m$8os7tv+K1f?Vi;t1QiDs{a^CC9Q1svk@kC`X+}5(WF(*>qW#@+HRR{r)w2 zkNPPSBf0El0%SiMx&M!VgA*=?UZy7cgxEiBfWVVIg*rG+9vX0u8jjk-kY>+_IxiC;l%U9RKW-~!{I{d9eZmq?`f6frW)^!4yMFAh zz*`^cu9`+!R|8mk#g0$bXG9S7Si@5ky}z9z9rAFW{V4jqmG$u^Tk5gPp;NW{n}pxR zQIFAy3D@B7k%#(HBYC_Ix0-#qb;?n!y*|ji)AnrGX#an2D$l^#kL~hz_(MJB#y!?{ z8CIgL=wZtQ$c_YFrLLmHbb+{*-FgIzqV&p_{fD+T8yAWzWslC+A{Y;uz)LPGjixsI z-emaCmREis6(06e1KUNtcO9lh{g&VS`KFyKEk9#vbH&U$dx$00!ZRIff;plUBu@uk zS$@h+8kDI98ar-(C9y50ZKB*;|C?cfTzXfBY4;D?h02OHb5YvtEXqLXCxqEO&+y&c zD)Jgoi;~Io6l6j5Cm(%u6!ru5O;&pcXZKs0zal;3_k{3=?VaivpYjkB&1Y{!y&BXR zBW%lc24`Q~__keN-T&bgsmzHFUh_0V)};PHh2#}q96i}Ar*>6dKsQNa)7 z$S?+PR%DXlcIz|t*z2Etu5RcXAy*=7FUe2 zvT}xu*vrP`zjk(pf7m7=88Yk|G0ld&Yj8RO zucs8;;iL_#J%?QQy6BoJPPZ;w!|hNuYz;V=#jQ)0rP|$PJ}Lc{Z;)PO**)y%&qxqg z&dBNO>z@leWlsH=(tk(_g|H2FX?-p3yWz=o{)|I8HF6$hIyB{INRBNqClMgcsFHbJ zk8`Vw#he;_#hQ4mB8z*K9!sP|6Y-KUZ|swyx?c(}zK#>0K1Cab6H3nGO(%hR+m+d5 z1fdmakW6-uASHe`flJ z*_h8yvg7wESIfmu_reKJ23>d{hTH0!%yuYYXZp8FI|*F<(~{O9#i7H`=3~ zQZ`u8vw3-XrgoWq5ksPQbZWrR#uQDhZT-g}7>6%FAV44yQYnP(n|z0}lZu}bKW8#p z!7pKFeMO;4&)?0s@Gknk@%vt31>4jkMvghdZU{4KvgEp1u@)F^+#pQbtL19!9aP=s z0t~5Ixo?okXePK|{iy_>W&2qo|J2D7cRG@d{3Rr`3?)6iSl6R~HsY+H&Ka*(5#ev_`Tioz`LYUtd zMCniIpM)GHNmJPfsw&jHO@8|1Wo_y{Swb1<^SE(Rw-7lwbsB`HWX9P!+K|21dp~!w zzAJe5PKD5Nh2d0HAED=?@~{BL!1_lf&kbu7K0l6==LN`(c+~8-`89jLW(_ryVy)hx7Q`O&%Z0q^Pd z4>E1O1LaoD0iG>Jyr-1p78=LxGg?rK7d4A|V;t1)BPsY=?=>Ux`Esd4PZ_peMggyN zl2^UO5=&_%+KFXu)pGRv|ZuPVV)?8oAtO$~aC!m)p z<9;*KLcz98N(X(zg}4@#2Veoa7;1TptMpJz42&)I64eX#@Do) zq$cp$dGE5z>128P{KP3@Imv2Y!gop^S#BzxWcZJn!Rgf#e&@&c2qaBtr# zDC+W0`WA3!%A{TC9nmFDJ(QdHP*_XHq8QFE<5u-v_Rp??i1>28!qyRs+#M9oM0;TV_{C!=9tE>nYpyHhiSA?HHQNuyqcLE74tIW#ijthxi;R*GV?{N+!dBnI)B zu7>8~%n<16>gFvJY7Gw!Q4_KA&|Rbfh)7G;!)8Lh37QFH5NnAjtmoph%fp@2DbB+T zbKLsX!Nm__^iLa@o3pdr%iyAWE>EOQKE!fLg`Dh@v4xjGH*WLQpbc9`M`?rA_4Svi zs;aAXCP>K)He@Bnw5@1@rHE|q*Jy^4*79rIs{m-2;j;d)a{lyer)s6Tsye?CVMyf# z8?$O^g0p9BEQ>|mR|0&xK5)^+C<&drZYZ^uOs`(y+MW38#zRN_frv%o_ysx4vdiKE z=I!Fe4HsuMpV{8b!BS~{$&JIInp~HTReNG`W~PWDLbRVD<}TM&iH%)tCVVJfez)=Z zj)hHS_?12+j7}V>buEVd$+eq#lk7>?$oZ0HI*A%4haP`sATlk7}a>T ztX|Q{e2ka-PDKQ^Z80@5fl!v}t)iP)2|F*(<}e6f!KQsH9}n@MGoI7(jWB#kex>~l zrC|za5!CD4{BW|-kT6W>)na{N=VtoLJFF}$S$Ovz5(dA($hUp<~?Xb5+1*kH*-*Hytr5oe@nl{?szc@;1jWNNE^IO}L$@GzMcH(_pg-il0M7I$zZ%@u=Ay8x`n68wDmt=jyh+I> zk$jib`XMQ}Aur)+&W>D$S&-U;ZTvB-r0dgk71Pd&m;`|;Ian(zfQNLq$~m21OZb>f zq|l>kZ**MBnWxi3{B1OaHby({w3M@A5cO?K%L^}Gq!4AamXwxm#hkE_c#$DvloI?( z?!rY{+sR4+K`QT|zCKn;_#1!}@W~=+QmhNw_6jU67T_-@&(SF#@7Hx?*S)n3C7_p> z(z}La50#Uwe_eIFuZQU+eKb&LqhPCntd;AJY7g7IW!=6&tqwao~$I)$=UPfA#AGz|NFHfFzvG1_07+D z-K_oBauKdiokNz{;!|d<$ks6#Sy`OhOuJ0;_CHe$aWTGx-v4+7CuW3MmThxuE7^`K zq#^N>kFQD}sraXoPuY}3jdz7bCO#wu$Y{5O_Wwz8;J!ZRwX;b=@QPI6u6)qD*3-nc zuSMN{mJ+i|nMk@l3LA6^&$;wKP^tLk!`gny=b|L>bm{5o7NKkeSgsFC^xYX5x>$3M zWF{|`!m@_yBWAc_eC3A^@n@z?%R8S2t)3kI59^YUNw^51PFFw7Ex;@@M zo_Nc8t^Cfa(cZ2#hQ?Cw4ekT^8JG0dOE)Nz zTvJ4*s>~F+(ckLB>$|6eJU^Qb(L8=?7l{1tjh$q_HT^96lMStB#*eNc*cCH8i_VY# z(6HtuBeF1Odv#}C`f=c;YiM%>eqoNv^xaQl`gzS89d*bHJDdYnTF;gi{=4l-75Jz} zg`X%GPeSiHj_T3bB(|-*S&1&`FWv-FhyHgiDhhq;Pu(vGH%VaUCsdF9hU~RA{h0RmYGbyZJGRd(bsig4uFa2Tqg4{`q&y`-C=HjD`Sh2@g$vGOgrZoCnjiUJy!$S9hJ@p{ z>7_FK8H2mZKMl0Gs3wuvod!a`ei2F#KJBlMez-E0jMG8OOy>8T)T3~vv$y(jdQpr@ zj_VhBJS~1N)1;M7U#k%L&5Z?-?L_LmFOl2<07c!$>qCJNN;l{V92yV*5McXxP?$mg zJUB=|0VO#pz~@834$3wAUl>sE1;G6{Xk`E47l5B1VC%2J5hLma_L7nk`|_dIR;4M3 z{_^wkDoFRy_c$b0_V({9D?I=Mn44?m=5{4o0Ur;KIkM^XYuXrJcoYLFNwTH2H8dn7 zy4B0etKg2Mkr6WjGQ!QxO|UAOPY%xcX=!7_N*p-hb7OXPHbk0{gM;JKl?0`>mX>nH zgC|9sIyg9(nN4lnx3I8~mG!rffP07A-MZP^+xz*)LVJ67HhQt>BfPPS;dcF&z3K?*3g1hnb%pwQ~*T_x9=8Z0?ZF8XD$l`@Q?oTA{4AZ^PGyfyc{<{y+OfLd&Sp zoH2@zMUQOgShjlRG4aJ)`{T>I$#V(%1{zlg|u`jqFkuY5%h1?fl-aJ8GuE-|5vuDG? z!Vx+4952DXsOym2(brdIiG=VsBqZdA zZCPd}8v-Zs{29OWnPFVT-f`NGi?93IUy#Wy&sD^xDXzV_E6g3wX`{;Dees6KOT`wB zi~v>k?&P#MoS0s=U^$a%-iR~jux^BRD>89Ev?n^jEysv=E_%?xyLQ=r_arxcJqLAl zqQKf_YHoSoY~|jvkM~(%>j6A@mz|4C30m38$~Dm418mrs>;@;tg$oxzJr4@*PoLaW zhtVQ*iG6>l=%CsT5^*r7aN-zkaa@x>sKd86@+q-A?fzK`qIA$MnG2hlv2<~9AtQ?c z#F&B&v`y)yvBpC_Pu#~7h!cG!@}rD|4FYLOM*xY$ml*~%S41jBPU#>_zW&?aXp{|Q^)W)#?gowK;`ye z-@SVmCMnvQn!pAfv{w4ji1F)Tw7K|zNW@7~1LQ%8;Xsj@y>C{vHncoa-n;>`cCh^t zda`8pG*MB}A_*9O;DkPuzIFj%Dk7W4P$<B0XDG5N;!+nRqFpnx*vF#5@t8f_q z2qM-S5bn1}L*!Jxw6+4)-+75YkA+ozoU=X%+mrinFFh*v?j@-xl*krKXlV^3uv!gN zY;SMNN3yI=c1T4}_ZLn;yDz(`Yzf9j4!D+`3*_KP1Mo|+1Q=OPVG$7vjxn9Al$85` zzVI9YSPHB#S_PV1)c{WIp&%n9T!_S?$~okyIX6GQ8r%5zcpCnguFzda2Zt_=#>Ub5 zATlc16JeAB3T&$YpIfj#lqG~VAPqBJRaPeweO}Z}fI69} z1!(bsv{)u*WXQ4XC_FO5>}OS1Xz~@dy1jT;_xkmx)q{XzdqDT%nCDFPip78xwoiUh z5La>(VA){_>IC z+|rTfLFFYjqj~se&YaQ6*Skb!744BZHx4M*>CAK^rebJ%!rk)5=utl4EzvlOVY($m zF6n<6myRV2|Gy)}1ZV~5-@c9>2nD*i{I43KZvc?d1GS`dK$4J=0WfKM(5B`lbP@$v zgs!5Ipa3XiRelX{=0j@a@aZA$J+^+4Is(gP9Ycuh(KdSw&P$&7RcRtB#v1X2+Tv;_B)u zy0g+XaBpXG)!Es(zrTOrjC@rnjmVdA-2qAKMf1+oK+CVd?OU=cg}A*k`W}m;Q)jpN zqW0T)Q*lBz!U#lJd3p5UeRTz^fg*YBMHnXGNQ0S$Vn55zdgbQama{p9p~<$mw}A62RE-90=X zJTsY$PJC`-TZcBen&mJYl@;<#T?tV8^eFQc{?@@kZ7OCKmM=e{P3!akfcGg;&mA(6y4?#4ws#$! zoF1r>V3@1e36Q(2dx~ar>+4c$g>8b_>1`3%#L=Dlk%P{?+8=I4pBNCT2 zK+nQ*os@=#2I!FCt|OPWiX1}2#fPrmkduSt8p$j_jj7ztIAK3ac z_&GF`L6C3FmW69R=5BnX>c-iyt`D+5q|fLg80GPd^!)kMsON(;MI>``a|)SIf-=V~ z*HG8^)f@}X{9&;j!-G}iC4e0?meu$NZ+IPT_*R=orIJC)MQsF$bV?bi2tJn->o`IfowgdFM zmsVF3_{}gjd7VP)l%oQCdwCt zpAwG?b@F2w;r*)PiA>7QGQxfD24LZ9Qj}x>MuHQ;9+=9ZB==kQ08YWcH`6)2k5K{S zW74N!TmVY#`)mrFB!-avZ^OqRU5+1aQRy=O!Z4Du3mdxZ?-8EswpI!8#xy*JPf`c3J{SUYAOSgGesBFoGqERh zpI$%quAmI=#PH%STUhYpn5FYO?6Mz331ugMcRH>-Ny>&!l-@W$fFSyiUbgt6HhT^nk{Mqy<_qp;3#|IE& zrk;#e0dzKuuw3Rs+Y(u1zx!!G@B8lT?7*m@N1qBnp9X$*rrj_ubOR$%@t8sCsUt`i zAn0JuDauDERSe2=3ybN3{FFq?S+Mhm{gQZUO{eS6lQSdWnN!nRe7Uy$YD$ z1C3c-`N&o~a-=h$v}nJI<^x7tFmCey@fQdDMe7oniX*9?_8S@+qTn+wi=q^EG*wfJ z%4-FnHH%XGw-nhhun1oWzeBjTf5~B}F>DJ{9Ul@C_4|i)Ao9(x80+lpgn?xeT3Rk4 z7u{-4H$Vd^WCCUvn1GTzyh?WN`}@*Tp^@rw6pE=YRpE^p2QrkV(xQ*cilL^{5*i9` zeR0riJH8?n09`Qd+ldVVuc0tIS~EJJyAJ3&bC^9`qKXD=_TLK#&z(z&dL?A@T2M|=F{&OQv}%jO35&Pm z$zeXtXb5IIVXE`^_JqX5Z=zr_C|OYM=FMoC|G0JeVE!=O*H;SWANhEBvsw|Xtm$O< zpc=urC-zNPb{fQQ>}g|)QsurG?@a~coLUs)1R=>+8p+81m1^z=6P8H7gP^9T_g70? zw93fH2#Nkn2dGC25CUQ_RX+0L$B(a?LvT-@UT=DguW`JdVpa#KC>20d^>za3&k4c28p zNkV8}Dn$V6eBevJxfr z);`GBF70NVV5!Y$Z`#uG@`I;X#V0FzxQq`YO)dha4NNB^O|AY)t4m|1+V zCx^(}fn+HhRD>U(RN2$0X=u=q(JOYQ(wygZA4fgDt4)U-3AlzXK-1?sC}E^_r9Xg( z3Bc;9_+tg}(t1WlM*YfrW_}+ofp5e7O1}Byo338R0bkK>|t(aQhb`>Hq@$M`ZA&ARPh#-b72> z8Vn>8Q1I3b59=aNM%vxe39tiw#@_cRn)5mzTyJyDoFO3SO?ws;1kOSXhFN(DLT1rg zj()bq2a2^X9Qg={B`YdmM2<^PP(z3iP=J8%FLV0+qU2k#MNE?sT+D<5ZlWcs?;@{K z=S>BO`*$Cy;9rEfjg5~ty|K)Fetfnxg(lBbv+*)2Ox&CGmr;#MA1lD}RnRErK5YAH3DR*N zczXg$5}nEFpjr?Sk&#Twzs6+6#l<%&wetVmxo>Eg`c2Pg$I0rJiV9;;f{05V`@R!G z`k|BWvZveD@iX!6Zf@$HiWewMJQ3-?Lm;MOD}fdvQtoEk3j3m||@L zW)`vS64`3&xysFD{y9kHK>lf9j=WsEoEZgJNNMCFI|g)qns?hMKHQWquBFqVf_F(# zX;-6|ngC!akyXLHPmg8kyo!>N zfTssErKZr#M}uo;2iE`sy>x|<@eOnwZ{NNRG1~zJ5xjXP01)g0bJIo-4TfS9#zD@`j)g zt>T<#gz41?pKwNKH{X0rlXHQ%Vlqi0ikQLJ?EEuQ-;`)+4#x;-R$k}-g7DtpO863H z;yFKSeR7JXvAy7J&c#SY=>+4=dEeQ(O5v0gk*za-c63@=hU!k~>y1vVp-s(hB4+yA zk(X#z?5_yt*S>sFd%p^<1du7b;sJoPcW>YB7#z$dDuW~+;(G~+M;p(|O{0S`9rhU! z8eAAxULwH8HU%j1+}vC!4}tmITbYV{ITW0zK8-(oxpSdx|P)>ilYZn zm?UOpWxag)5^S9LEhtPtJk0+A6NljC78QR+T3WqM1|=2M)~^;SUIGSmuyRnE2&(SM z){K+2Gq+p(Sr0`X0YO1?a+vvKS%o5nz(o#@ub$(8>o%g^-x};Ro@QWR02xM?7!i?7 z3VR}osP#W&_$B%GansY&(sfggSuzd|g#kG*(8DBX z(n?KaS2^r!G6WhoM7qs|977wc10NxxrlIjn&d#nt%e6|6NM2E~yHTgi_S4cuE>GlY z!~BbK!h;H$b!zMDs~n6nGBV~r7G0%{VE>el0JtB=mj#Xl_R|;)Db*c=PxFR&&3@ZG z-Sy&7SVYG+yV;?k1pc*)H;kGs%t!mKOV{PB6L<=72?(hB|6Ja99vKO@8SUz--R1_y z9=JF!7^7rlgy|dej`ntXM#$5J&pY5T!ixY1Puo(E9iNvg=Dm$D`1}MyJbckL+m*@N zG~LiVb)Fx|mH743zqwhFH}q%+tt@YUfSb%EHRfpzZUX1jT5+Ru-aT(=$v)<55BILZ zjT=os%7X$UV(Tpm$}`T83()Oh``$7uPl!>oud&8MO;3*U5ME6F3`$zw0lUe_LsZwvW~X-KTq&q z7`mz3><1+pG+jHoM@E$XS!HfvVb@Tox%Ie$ygUr}&jL*x5ip?+>0#>JoHZ}kG>+2E zs{LJ|D-V8uph^V+?d))+=9Vac$syPMrH2y|JUXwesCYgiFOTbZoxH7LkaF?HSj2La z@B?fIhGwu%J~~}v{XQ%>=!=frQlj3`+RNmwP!l)=W@&Zz_C5<3ob65K7Zfx$GD=NP zhxa?>2_2sovEVAo_&gyYmlPi#pYf-Cx#KNB!Sm&JRr&0_{!2zGzuyZoMo>^P2N}gB zlD~1IOCmZZCK0gh@V=8{23PiUcL&HB7$j>I6%_@@L80;K8uX;0AeX@tA3Rv$WRZ#G zz_8tRSd_b-@#c-BG0Ozm5ZIj~TK;fTipPW!hWKvsn$wMh6t?0gRc*4F6P9o!7hlyPxcgHnN{SO~|(i;Yc9 z{T&S-?#0B$ZopW|CqA?a9OMoiqa0>7HhmeV6TzmUfyq$(D3n^fk{=EEMB8f2#qXxFfs!7JjZ`&^hXg&U%EO}Au$;$NtewLZj zS1fMgbTU6DzsSL2_R54V%1peALYD&DaIYqNk!E79a(&rrbnAnYlarN|m2Wcw-1VU| zihcW*M6p$AwnI8e~2ooIp+y|+L5HvMw}etw?_RC|EsQ|Ouj zgoc;+0;i!038@*pizUjj>#RF-AB;6&|N9vHs}C(;9MJH#hK3-F_g+oD!PxGw0&mxO z!KKS?0JB2^$e}6^myf~sbxt3)XUN+lXIPcu%Zytn3o>$Zi~B8MsOH6Kz_IeYeg9tN zb45jkrC!Fx<-enew^tS88BM=h- zn?W47UU6vVnHE4Es?Vuc9;P#~w^!qS^OOJ1L7N8PJnW1K2?-S)=rR+gkq#Y% zPY2B$A!&GIpCRDsz4+km8CN?UiMw)>li@vy=~=sD5IOE$LVc!EI)0k}o(*y;Cmc#u zJ7NgEd`)RbI;qi0JF!G%G8mHQ+=0ot4x4kt#3vs;d@}WcSQkoLs}IkrQyhupoJsO6 zxlyNVig@VZ2g#|cNB@dJ>W_Mz-E3CdT%H>i*>Q#u5yOse-!g)PZMoc$I~$rQ)`%YW zzwAg3(6e-JQzSb}D|(R#rZRqW;S^-#=l3OY-g!r_g(OjfX!StLb($wHfj#6eD1%bl z%H8S;5%rYZloYAY=prEp4BrI3uyadm^4W%D=}_4U?BMMwsi~sOj`Q^mG7aoCtDx^ zTRYxUH{&e9(ZX!4>qNk~53vkjjp0EemO^Tyw0ih=N`!&c@BwX-|duOmT(^>LAt9&d~0I z24z9}(;M>g#|^^zu-9+RO9$&e&pHoUmVg+^oq{>#c9YCbZE$1_sJ|!rf^$|-VCUpK zA0*;3M}A)^9>G8~12O8-SQ7~2XJKJs;&bPUw~zZci%BLURQF-pjExj#i+W_NL;{nt zvc8bR+e%(Sy&y>L;(2y<_JMZ>nDCyK1KS8Mfs7Lh;eq*CB3xic(2WI1opTce~gJ9JiHxKuayi%!!lEFMm>8hL%B0JhaX_Pp6#X)~qZp6nlHg9p;9vKwa}d z%gN66^z#Lo4KR&F(dIxCV&q;5=Aa0NIii<`~?WNrBs4P7B)7eZ?#QK(!m06 zTK%uy-UOQJ{rw-MGG&&`B$<<8o3@fE^OP}TnaMn)3>gZUw|U5vO6HJcj3_f9Br~x| zB0|V~pN-Br_ul_+-TPm6t@}M^eV4;$fA(j1zn}N>exBE$riS``1Lg%N^!*)`#_68o zOc_|47P|_ZlpP%%Zb*hN{;!4|YHnwUJU7_bTC*XrcXWhZPB2pvs5Bif5a%3`Z^!*0 zO$!djReJ-?NZ+m1=G5EXw+imcy6EX8lkBLes9-3Mu&W_uye#CgWb8}X`N!n=hE_@M zZ*&$YQsSEW6tUMo#0|W;tJlPf2sJS^4G3kSq11Gb#O*Z|AkL%tct$ngoN1HwCsVq@ zcGjigKqKD!246ZS?wi*3@U=P4MJ;!x;OQ*-)t3y~8@NTG+v=OoZ0yuo83ev>sQ5WK z3`L%!OI`y7hyTvQ!|lLPqZYH1J$|n+SpS*F|L%AX*PdU~v>tW0Mt^SHdkZDqqw(q0 zFzZGe-I}P(Mnkmi^KRaqdmq?G9V)FAj?X%P1mPgEYI3%~fk{VQnu+6_cAneijG3_2 z`17wtUUu!A$26$gkj^eQD<_xvk~^nd{#dr)oWS;)kCeZ#E{QI9r>`3u%^n>e&7~ut zru~Fdl9t83aisPf%VFBFM$`Ga4LQ+D6kBfQcyu?kt-9SikoP<4(;wy^m}H|ix*hoi z__pwwr31n4<;_kN4h9mNiSNGI8l0@xKi{p(?T8L}!oAu%ziJ=2fG?MLRgMtA$wA8N*)=(!4PzFOvU!tcbGqGw7= zM}G!gh8+(KPom(#{wg4D?Cgv#Z!YuUhkViFKKtRSQ)OB}izX!Ny}iij*1)j&J+cc8 znVFfRKjm*lO}Wqj+eqd-BP#GrSvGf|EdzWZ8>*4T7Lnp0LN7zOxNluXn{%Ti*R^xD zZ_4GX_rxfNP_aG_0(D;sRXAYx-t?DWDBT*jYV<8GFAI(o+-LDh*zK6 z*WdpF(`F83+Kw)S zjA^=J-rX>(_qX@e4+3jj;<1Xm<07uQDJ=ph3w~5q*Hzx&^t-?Wvcw}ePlOvo*;13V z@UZFXm!iRiUTK14=7uQkYtjm6AecYXkQy;$iLI@#M_UreeY&#cv!7WekZwRcH-yq7+87UvOjlse8M zlG*LXz6+nWm}r(HGn_Rj;yah?cIrDtO%!(syUv($9_T#N02-8LGjifv5P5)k0fJqf zODJ*=Aa~kXi7Weo!ta&^$r`I>-Ml0+X`fEHJXU4T|41v0jEh)xv%?*mFV!bai%-fV?Gi#yOA`54kBh6HA3z$^m*KbN27u89dq{hO!=iWV5EsV>70Ngp)MqJ~`&DdN;E zy(uf1g23UD7GZ(|yq*9jGBD(Uvq(uHN}0O1(>SqJ zmP7u)Y;5DCWoKLvOVW!+B*8BTQj|l8i1&!La397LUBj3A{)5`ZmE1!*5Xs3I@TNxb z+q5W|tDDzOplKDSh{Eryvz~t1RnOY%JnHT*?XA_4`q}ERq^Rd!YS`Ei!Sg@o9~T)P zc_z-a_kCdW2a~d4qA!Hs*S>nmntkwjIyXN(9binJ`AX;$uoJW&3HdA5o0ThLgMv>? z7+$xK2qFscrMtU(bj}5*>r63o^77ZOSh@7x*cTH!RvF`iB5tjxOn#8|x%d&~UDu70 zW^wWGNM=phOt>zQIrpQM;KAlC9xDdlN*#JJgCGeT$0w$&QAl>u)?^}I9HKjq>9cx6 z=}u@dXjjo)?7BHS{2u;T9YdgMNx?w-i~On-O*UuqNau|tY{ z>L>MtZg8S0!($}4f(U(`vB`89STnNzyi{)CqAavRr10Z>yxrYck{Rb3=PHA6%6JtO zbILY5a?CnFlvU7-)qVEpJCdI3aI-bKf}YrNv)oqT zBXxJ=qf8ye;0slkGc-GzPAsx@_w>Y1J=N{ZVWny?pf@e%8aXVTpXz{F-aGEDYyY>yZ8XM-eVoWy4z&$fUirljyLLN zaS=0>`@IY=(e zVD4C6fQIOH7~#nE{XhFZdu(W<6k7<|t8UYhgiv0z5l?bqzQZ`vbSG#~(8|Dght?*- zTJEgy+l2*tafIi7%k`F0n!tkX@o~ntJX;Sd>`%_QqGg^nP>^U@!J2;KeS3TR{CqwR z^t(Qk4)-w9wV+)~^<^hN(JR|O#9667X#d`%)gtgT+-s_*46HSQxoLG{S$LVbgx!F$9 z^T)(cC40xL?zqmIX7d#azo*y7Ozxf}VN6dl7g~$qimWRq<4Dl#P!tLgl@tx z?q5G?FV292LRZ{Gzpp?Q{J3{9+n6^QG62VZ&eEdnDw4*h6<_^G*M+|6FNtkUm2NG} zMe`J}qf}T?ftuQklM;=PMGJf%SAYHzCGF?)TSAsQqR3M1>1)A;-uY9{mTe8l3kNzH z$A`D}_M+$r*jIheVR;ex5+zE#B1_g3O%uR3p>Vxnf=#tM5i4?8oQZUY`_;qSbC!eQ z3ncYc3>~$xcpK^v;0cu{if?ZG}jXf zwyUtA3_u=$t!i%3;u9R*RjIqXn8xNv)6GNEg3EvZ|K?T^8H0Tj&xj&~j@b0ELgBXtJN^$Vk z_@1C6t>5Q`s)*7~(9?u!(2S81g~`bKv!qlPUyuI$ekZHQzoO-X?%DYtl+sCGXp}?P z+fZfuVND-{KfmRDSa z3U}`yh_2}l^z>YB6$U^xiqt?}L1AZmo8fv))=E!LPji`AsbWsSkNy33g*LTkPvZb4 z*(;wjoCdqkuw&K@E#E8i>}kOdG&&2NYF^4ovPUf%~7BYbHPo{_izhv~zLMxRujyt5=ygTru9;`^+|V&4izZ=+ucQy|!?m z!lKYKH?MxTynDAC3RF?&cmX>zM_|DzC*_`3xwZFEu3^XbGh1w61I{Om<+h}PC7O#Aso7>7y z)T)-6bJN76(<2UgbydHywuFOap4pXi-}?PUsHnca{zjn~Z%k`IlX+=o=DAXGPaua- zgzfI{S4>~1zb(YQq4{ND-32KwCdMU^d_P3S20^>B+1je$?7TcOlG>8^ikk{n6Ji*~J;IL+3$<8;aX&UYasRa=f>A`S>D8 z%~Nyq&D{qY8w0Bj&?|qsx=>XIb<#|%Z_3KzRoTykH34>LVWAMs?&@A&V{6R3awd0Na5v~|^za)oSeAovc*B~&Y56mYHkAqJfZ|VQ{ zPPfzgj&WJBeF69L3BdACQn zez7#FFtZTdGyLQ_$SZLEa%5>Ks-@(;X=T(vnNQsxh6tA5q8kfezdqIA1pa>O5l@FqJ42l#t!rsr z$lW1RFfuZNkry}!V+^M5{rmUN2U#Lgl(TEbi_c)#J#EO)j2ngJ6%`^rY3-IU-@&s4 z|Cv_)AI-Ce1QKZt#DxGWnVT8+b*4y_Vr|)`u^!hr?V7yom-HWx0bAh9|DtFF(v+22!wBuY~!sB1- zSMG^6MDJf}6*#;>sQAIf?wzxn{IG8%C|D+0#h2+j*>Pkm1EUEB&XM`d(cr=i7|dWf z{559$z}Ue;EK%RTR}yE((BNu3c!#R8amu*CtBv{vGwX?>XI!@(FS_Kh5ja0h;(ebj zy~=HTYB2b@%PKBQEHjUS$#%bvk+b2KxyEQbuLh+(NfWaF=Wpy8pG5KX0O^RmYT;s! z5pC-!<8+iB)!{GVE#Mb_oovuXHe)|mcPHv4a|!RLAF+>iuxTsbU7ncCm>*wsICtri zQ`a8EXgzVN!lI&}G;p#Z7##yLPZXE6@t=XND%cd}>-<>vu4+NA7#4jygZqbpqxFzG zH1XW92%ZysSv-*6uz&=F;Q$?x#+G}k%u8wHGu9jf z9ny`)5)i!M&t1M-mJNS6gNIf71jDPV%C|O_$RrI>FB|@S6U!4a@&BCmm{kPyBPk3G zY3vj@?SQ%mqS%->TK)np9)_TXSSnmt_lCSXQ5mEOAb=E4-K~O}j1|KnRj+Cj zL$SGu0)kj#UUQzF+@VcJFRwIU3yCq#?9sKFF_G@wP4((o6QAsvg9`9JC4GNscXxN_ zt%K}pU|=AaOHh%KX{4CU50*><+=rUi7`QkX8AKZ^tAEOy4s-A)XhiRV{QBLn+FEgg zkH3IsV7~xj_A`Po4@MjS&kJoL@QpdDa1M6^%*DiW3dfOoa|XKD$T$1P>?(fzKCur=;rjkP&z|OO6vH^WBFEmJ0+kYi)cihjDcALplcq)62S2P z-UO2Vs%D}&NTdabfHG@hM4#PAIFPcqvuiw-d2?CZ!=Q1~8L$iN2!MPZHGm6}R$9`U z&uDpxa8u8I4XBV=sw;P%yv4QtJu4cZoTk8@>T>}t?^N2eO9yssB~yOX2~jjq-DxA>_Msk#PaBt$4da)tz6n36x&BTH$DBl2-9+bW;xJE zz=44ETvA;8eYbz_fPk1d5kEf6bwr9OxgY_z;I^wT#aN9(=zG>5G^S6-3|mYkxEP5TKAQIzQHKwT7GuKB_1AC zUeHFnnLN9@wYFN~c1C?tZU2JX5}>}1Ry>;vucN01^h^eDp>vw35fK*FYgO$s6|lPi z=$zlY%4woj>2HgnEH=}BzZ&=w(0CpBI6m$R z22M4`CScSA5J=w6s-@jqdXAlS4Aw;ewa=5x)~@Azu(dXE@Y(ytc}Sj~<=Bh?C4sdK zB%$BDhs)=pCU&%$hnSE|I#%V129yVYOGpU`0Tux~*{{-KPYg9`CW2q8-K3AdP&w-< ziorSLHkZ`zb4$a%A(t340H~o|TGAPpnaOuB0w)0;*is{@ggj7GMa}+BR&SkRmRr?4|F|BvP)`b}pLBSQ}1V z@wZu{L1G9fg+qt1YiJsmdy1;_l7R6By}r0xVtfI#Y)2d*KDd>9EVII58%FA(Q9!t zB^x&F20ASVm=4iWD?~(yymQ~aS-UPtb780|j*sHwwK47xlc;!Yau`nulDsor&e~3c z^hRJJ5bJu7ATI6;M1(gFRU_l;NT84V*%}mcd%C;7SVO-x=61a8g^|UObAB-wUz@=E zez4m5=v5>kb|$yZdR;6A za4;eQs0|HOF$nm)_F4G69veVa0YC8$*gj#^n5p&z)qylY@5aLGH~g3sZ{&)&sNOB# z))ohJhc=5ZAVS=70K^Im(7A9$8ZKUhf}Fh6yg3}$IFC)d3#D$|dXgR!<5&Q4l&^aq zULpZ5j=8{JN(J9N3@bx%-W8r;%~QXEZIEt3&MKUE!tQN25efoH*eg2Al;E@U+ ztJ<%oGuLSb`P;&aK>L;Y?AdwcKhqI>03Vfa!G6_qGPf={7hW@lNfKh0?#g)C-E-$> zW%+yPFehlia?*cRNl}po2tJFwz{UQDa`Jg=Z7xv@B#H-cVFiwl0FR*lv#xjUs9Xnx z{=bJA0;fZ(1M>N4rZnTfmY_2PI_ zS-Atea|)a5EXM$pVMC9h72Q4hglC(e6D-KeTw*zPvA5lNhldS}I5vO00ALfEXQdA3 z*VooVHjxH6H~=6r+a&PH{f(E#K+3x`R|!<4@~YNIdXNz1Ir)wnYrp-7B?f~mgl<~{ zQx2eqSkMmGEsvWRMeP&{U`@YN!SHYCBMD1I7Ey5?tDVKTm2)k1``ZhvcYJ)r|9#`d9U#OEk^T)3Sx3GCtyL34%WNO{Rvh`Z z-N;>;%S%_?FDV-~jt>C!6Ld_?Ig7EX0NxjBi)}|21cQC{w(s7Q@r$|Q0yDhthWpZ3 z3M2u4Sv3xqfCvJMHK0I0v+>@l9RkB0h@^&Td%$H?=2B2lxY+>&8Nl34K9xS1{TZTw zmBPER2>_XlO#-I?bAM7T<7Kd1!?K-np2g!E*9s;iRu|=UUxQj||v( z)$~}v!crhvT<^bxq=dx}V8ioz9}o1O|Bm$~6F{>Z5uFY<(lC_%ohOGBki(hwcT`|{ z*znQ6R5}PoM__Tdu)>*{86cIW;2Lkq-SXdEQ2=>cKR*dtJU|5Vc zm)9lW;fUB{$Xk4C);NW!3;K$O&hA&|C+3CJ`9(CGD1R>}3^Y zDlFHz*i=@^4Ky@jjKMAE1TCIU76u%JsWuEC(dHHw*+T%)HrbvAX#R^L8d#odX*|G0 z%TdI4+dVtM7-ryKTF)>>C73b|!*j)uj3S(zoND?267np;u+$uf7H??o2#1fkqF)+T zV~~Dj4?*sKi9Ncl8E*MU2TcDwkU$22q$(2^-{JfMSlVGR#H0QOsQfo$b_kczFiG6m z+6vv=ufaS5lIK4+_8C|O*+Uo?Ge_o9E6+Z=L?VWHjA;Ro z_J`Rf{dNF6|93!i%Ujw6LeWD+AJ%?6tad9y%E$xWvlfvz-t^cyjEQeoP zLE?kqfLjBY#(A(fLqr8wSK4_kX?NW4y5GMy131#bP4lI(YQ#RqEC&?EVRQ4(#i^T` zeg#UNvm_H_!wZ5v;&%Y*?MOvV{w(1~Uv>nqrm-J`(}iMQUjH=E1&CL0 z$+-g(>5lC5%19PD6+bF%BClNOJjeq& z;+G9pU_EioE-isL0U_ZmWbqbTv|zJS+w))?jJ(YzbO0kOn;jQ&a(a#&>qwB2)IkkWL)9^eh~0d+59KHXw{jXb!_40U-r7D zp4M)o%+kXP3zSN!m$$cFAn@d?zt7Axz5PpKDkP`naJ^&mAYELnZv3(goqCPhQe%6* zZnXsmt&Jo$Mv#E30_f$ofk02F9;$RUL6F?@`^#99YMH{eOlOcy49Wd6EkClfaq03_ zWCg5IW4HTWC;^CCxpI7_J-+=f!D!$?q+sR9ONuYb>U|1MowoN8e!G9b%dm4^$8iI0 zP^kr2OyD_r^lTWe=>F96@u_7{PAo4N`PkT)1zX2O9h~F8(%LM8McOcAvJi6G8Ik_} z{$knZEgt^!Z7@i$K?;)oF*Gz3Q&#wr`p=nx==2Yd3$xvMx_A=$vzT#xGzkvrP#8Xl zX}s(C|K%SvgwZm2qJNdskJ)UE*HT@<2Lh7~cNM_~*4@KH(seqhSnJq* z<$kfN${ruY{C7@(3-3M4XEy?Ii6iZ4z@TIFjqL^9+#(tghsTQ_2CY}$Vu-Q;=T%iD z`p-w?A?x0T4HYpY&^ZZU6EzYJ!N1dP7YKTe-$0BcAyw=S`+q(_1DNESSyl!{>@yD^o3&|f30@B$wZ1#@_y(2H zo7eJFripZr6^Q_(${b{hznOx1aQ+-`q&tzT1A+ zV`0PZ>h{*Ho7We;e@!fJ?)bgCHU+!3I1}KkDQoNLaYVdcmZt5SL_wPhIs;Iw6z;By ziHSiH*AoJE{U{O!x&tzktK#uvnryGF)oh-c>T0e|S$X-6{b117$G%sv(XyNb_i=R3 z00JMr)Cu_}S2j=}vWFm6BT{iA8~zz*_)|Dn&}Zr za3>2j#;dGtXviG_lTar(H7yO;WQCF{MR|DuSx2B{USMG5hwJ^CwK1p@KmrH80;@mY z;fcL?|0e>p`+>)_y%+}u2MDJ>Um!0!tX~9cw}gSk79Jkn-bY->Qg6&W9pG!OfMUys z4>G>)V~|AxBvZ$pUBRunwe=j*<-sAo{xDGxl?5<02i^h}Qnohx)n}M! zW%k?Hs$ZMFDQ2`CJ@+w7bhhgntf*bC}pA`!(j9prGeqLS- z&*&?&K*Z;fksrW~Kd%V$@tEF4Z};fYBP~-{;I&iXEw?|1+AM;Si3z~Y?9`TH(HN%x zQM0S+ecQ*62`5G&ck8S5T#>;jD(D=5f)8fgzY<5kd;bskJ}?1%wQ;C``7X4+zW(|< ztft%No4@`13Ej^XHuOw8&p&Sq;S|IA0?J|JR@l{h?04i7wC3mMCnr#a#l=a@5b_KB zVE$2^JzHuk<9Pz?nh9M_)vDXlO{WMQIlk=CiYBOA=h=gzE+WzQ!FdL3PEt_EKUf&d#FU{}OQMaf9Vr=IPb5F(Cu!=FNRQy+NoR>gnhNJ$i(J(u+3_D$2-w zfYlX>7VPK%Fy)4F(&zEh= z&S&53OUQZgLKeZx#ig`v0TldIJV=OWny4_b`Y6Vi1%SeYQjFt7t${6Y?5td&?pAOP z%oSrL|3E7Ph-YAh2u?Bw^Zguzf|w9@)TnqVRUxbxBPT1%spjZd!~@<(dGFx$zI6x9 zG|09LtKF;&4O7QH#<#wP33k#teQfGtpU)sB?S~)<(b#x-@T;YHXrE$C`!uiubxeTT z#+^AbI=Zm=D~Zp9xAHqsEQkb)VVint=B3zz|=|Lve8zr9wpy(BM>GaFcYadNa*o^QjNcp(!= z6hE}5VS`<8HAL83RQ(+FhgYJ<3z_6^r*w(SvcTDtqQbCGwxLnak_j$L9WP5u!C*=N_6sRR`laR{f(YC$ z?>|^>Sur|>5!eW?QtEDGKN0uR%E9jJ{iEeTVYy8s26Y=)b+fB|cPbX+K~q5w7WgB7 z>yFM&D~!wTAdLD6&7-rC7$iH)USM!%tHSQ>qNIeA*^uz$r;FvDtrV{paURhH>d}@2f3? zmGAm(JU4!>ft@Ty%@hP4G0kmouleO~33d(9&F1X!?WX)gKR3XfWfHLF`8vwE{kB4M zU_r&33|n)!GED#12@tzrJrLar?3vKK8-;bXweFW0DLf=HQSuhN$yYMeWrb2yDNoC> z#2$Cop*Q?C!xFLe`}do1r}+CH=FmGm&Rc6iRJ5bm;}HxfaBrPJT8DH zX;yEH6vmoHI8BXWYTcWWq%{|Iz~#3}Ff^lFe1Te77+ zFzdEQ(NK+beDRynuZMXoK<7Rl%sXb$t*v;z?LY4U5V_sNyAE6i{Nr6Sr^Zge0LHYb z-+sUMw!Y+!I8zvy_VBj=F@gXXA`;w*$`qx;4m#YrLUiNHn=78XD=V?dV=$oX%C|40 z)79s$W2X~h;0c%;YbqMFFC5rLUq;CXhGM*Sc*yW$8cg}WX0=gydp6Yjs= zv=gN=!Is<;I(ID ze|ygA902c6O5b=wd^l3ZmOWN}dy3PcTFguTtbcY8+1%ONyjZm5^W>is$FFTIu6r95 zC3)5u^WJ+~XIK=TJnUf2Pb1D0?P*&%Dw5_{HXND^&t>u{=jngGM-tH5iOS9B6@VLr z$-;~Nq@fG`Fj1`*&jc7qGSqF54C?Ih52cu66&a!x_V=lebKOCo>p%qxJ2>C(_;$R5 zG@RpaGjlBDA2uCk;v7?U4*5!lh1a8%6f^A)6XYWz;s5EVNlA1PXw)xfs^`KyJxZfV z4_%5Lcg_Nn3#?VZO`;w&8djeQ(xHNG0ct;6Yj}B6hA)xh8FjV2+Z{L2N4G2=bzbUF zpo%RubC{rxh3rA0#TQpsSFdnj-TSBe<%7qrzl@uy06<5GCJel$;odAOz{!oiLGT=Znh5!tGe9{-gFl+!uAHhVgJ3vwD8U()q@)1y zJLYN;;iow%aZkyOQ=EMM8dsImZ9~tuQ~PR3Yr$r4fAoC*+Mcv-Q|{k$KF?DegmIMhx}L*O2iXg*PSQtmC|P4vCXk#Y@5Mo#UFTY?kc`Fe9v z*VCr2)91RXqZ!oE;mT;kN~b+=asg|d2HW(0WJo`f;4!Fr)~_{!bG?x>oyVl_X2#tw z22g`~ClY)B{KSwf7i0SO#|P62m^an0HbRYYO;4zM@)ewjl2#p^-XfOXauzq&6{IYt zgu3BhIKGNy3VJFRv`c06K?ndm2U$OWi0C`FS>+;?^ITtZGyS|lw1xE{DU?-rmg|!v zEkKe0y2quTTb%3nHlR2${~!$=e_bN~PI~>;H+n9S#VeroS?{wxpl43(cWJA}AM{`I ziE&ZM;1$Zu>S>};KDDZp^8n-JfQop*cqlBHC?$+Tom%&lK z0*H%MenkqlGSi65D@|uwBWbVUb#``w=n)6`gwI-+MU>dLnuX%gJ3Uu7N9_6hR3m@& zDx4d4?^lD3@+dfEvju|!nX%VwqLFjmUGHj#kCg{d^7LwWfDQG)j@Y~?*hi;1R!B?Q zJu#c9LPw#qV}dP!O{ppw=F%lRnC{v_TPs`ew>^*iu zMj9%lqkzWyr985;l~P&w^6 zo2WNZ;h6Tq72_Ow=~9;vieAF!PrBG0)(<$;#-Bi;@Yj#cA&Y4K4>X=ofV~ITL7%j; z2NI47D7x}dr?*N(pxV;9-&opv{>kWNWU7#@{e!BB2+FY(LoF zS+?J~=`r4%E;;`q9;qX>K9>K(g>Fu$_~D7k_nHcfcRvttkZm@kHfI|5f1sM+w>8da zHNRn|MC_2_5Ff(erbvPcT%3Tn234ny-^F2U1V%Zso}iyPeBs#%&+n!u$ta04-fNuC zY0%0R;*BEeMV1F~p&}a7Tci=w8*UO>4t>VIQu#IEWerk11I;9UigpC^_7J6j&9_fT ziAq3`JnpZG;H^=lc+djo(4*x?LzJOK$O)h&p071lr!-OhRiYD~fp^xjq9yE{6q;s& zUD-lf_Tc8`((jN^8|1n!>O#Y_X8AfL6}AJHPJhscO?vTt)gwoRiIsbht6YvYff_aS7TLySVNQT z7&I<8tHjP?8N4MGN2z2@616Ad zV%LO7=6G_fC{N^Ve~KyH6eXd{l?v-=x<3=E5cg0y&8dbflH;lv^O%6{ldd_c?>ZzX z^aA>%Xs)ZQT4@{^<0{N}gc*&L)kjbPe8L2B*G=_hcISVLd!kX*J0AotWVDE3=J7@D zm_p~u@q4MuC&i(#AL=Dl;L!%`k;?f4R=%bp@Ws;MFS^g`(7w#bO#| z!>&092`#~3u(oDD6Kl#LIOnoGf8N@(8|{BRv1LUw{*Sfosps8dJZ7Yg+uhPELhqnC zEsjNnDEu_Oo+nNBfYSpeC#)V$ zmsr!G@DdgBnY>=fBIahA6zMbsR0z##p!iYsKx4!u^4HTk%@4u_kQ~KSMKoox zYw{PKy2fAlUJl*1HxC}oyh*#@NY@%V-#P$QIF?T!f-4`IAu|1aM)#KI)T5Ut&9Noy zmRQI?4fb6*MW1d{rJ|wzK!vyZ6m%Flc`Kh@am`|`X^G2GJJ1{GI$AZWrm^qxFKDX& z;2F~wdbj98=F3K?MbG9 zC98Iy{TJIG7s^Mil5biUOR-7a^*-g$kZ$=IOUZ{6rAX)W_9w0$t#qkofyFa% zq(QM|N3NA!p1FY9uf+Y_Nl6au;%MYFJ{7}2Wa`4{U(|weVwqShI&?N3%QHvQ$XN4X zBMyhDc>P6{pO+V2T0CifviG=P*FkTH>D56#PL#!ETWZtuGu;i8X1ojR+Kd-@s&&IA zLM4d;&cAWRPaMKRD5Lr1gu<}zs{i`bJDDT!sxXWkDpQU8aUv<7X=)XSQtWnul7C@A z@#q^Kf4Jflb3@WCPVz_&ErH1}FPSItxpD*@@$phdy1~CACVCb89_)wX{7HRvSrqk< zA^c6_-KwaQKc#bQ8I02{PMT1KG`i>zGTRG@sE@3~t(8XLe7f}OHU2tVJfSPD_lX&r zz|a%zBh_?BjH=JwsKHO6E<4zg?5T%ZhEz0`l}|V1hEaF{&{V01 z`L}wQ#Y*hWT##MOdn<6EYATqXhWBpgk0jNsRJXE-85IFf16(2AwT6ri)^??&b`#g} zxR2G3wJU4%vh9y@ry?~nj!WlW()JG&xLtPMz>i{G_MJybWvuCui9aV@e@F^R<{ zynpuOMQl3K97NZi=WXuxP@pdQh@8mhBl=b^9D0TWL2Xbl2l>OC3tgrB0~exp(ieJc z%#yUXgLM?kR=*IOa>Nf_tyM9eUF(r?X=U_i+AFer+&Ono76}j${muD+rMyBP#J$y- z`0L$e^)XMjdy&8Oy(UiFPuwMt=Q<$4WlK7-=l8@TvYyaS={;YmOAloj5nC{%AW8DR zekWf{lFQZ2`6t~ro+=|(^flu@PdJ_j%tYrG@VFYmu&{9{73-kH>AvEFJZ!_0i zI{|^aKXif@`c(K5X_$O|Nz;j0{P|O*U5?G}09A4=qkaF@=#8KYM~U)jVb9+`N0a{q zR#i`rdrYp!y3*;Xy0|f;RnSwqD4KpR4D#`2(|oDyc^=ODHx)kn z=M3P-)MM9i$U}dcR}c1)1na-HB%t+BS`RX2jEML#6snoM@=>s7ak5^ry4*n}^~>ka zF#=HoRIOIxgcrpWSCZniPbcb+2u02K#L+2=$(y)vp7Z?ruppo1uHZH0E4nvm^^F_N zPl;TbV60DZzce%d>1w2HqsBGzHVcJ5J%|RTKajJmy1i7_$l60~Qlg<$p4MxSE<< z!+wI%%)wD5|Hp2tSoiMWwS;c}B3T;}Vyg3aAun(#3Itv-oX8JR`9`j59^(~sukV|{ z2y_O@_L@HILZgK_KX_eS%Hr_>7}|ewBMfF95NYRYSJ1W}b}*?ZxK8?atAUqy_R*PC zcBv%cQe4|9MICxYdC0S6^2q2r>&O~#T6wpAyj)$sb`7%UQm$a{2={PDp2bwlliy6P zeH(x-pk=LOP(>+|T|cFRrN>-X^qKH`(|Jcvy+obZ2og8uFQy*3yh&7813~Pcnb`7J z#!@{LxUUGDFz9FgeV#Hb^58Rycx&eI!EKrV*|@Dtd29n^e~JNFZsA+RGbj7mbEz9g zatuid0qTS=C_})5zzPXUa6vXzQJ5v)y!Y^21i2!0>zhHbXn8dikr1K;vMQ+6HO4hl z*7AKpu@kuS+Ru#~#Sl3h-9o&9M?x(>_nFO*Vu#(f<&_RwI;1LfkAEs zM$Jm3P$O<*5;Vb0cLV)hZjq)DUpIf3`Cj{((~zgkR~&yWzJ*tfBr4bRNC@y-L}ad4 z44D&_o4&7PJ%KW&|I*d8$UzQTG*u*g38%Jon7`5)5%LMdv~53Rrgy6`uP|E<+0H`6 zQ$9f5gUtt>ztpQ|vJXz5vS(?(nIibCDU{&TrBf0pMiM9NFro~reY|PKP$CenrMBmP z@=+=S1=@i!_Tn*T0WG6<*J`wja*ZU6}&UxP-1vCzPh*gbnS}Edk zWewITDQZY((4lT2V)}l(e4txFbi4F&%%-v3N%Ig*1=VMdypdNqd?S#$uD=ztuyT+- z_mu1lQ_nj-ymE;1@fWznk2Vc9IP*%h&`n%7rWaRBj_tvV)qD`%<6jlBB+^H}+OUlX zCe;RL0gg+RML2X0(HgN@H-(4Hv^MafE%!k}P`fWgq2MwmakAfS@CrB2z!7~sN%^vj zhJ(NEtA%iGBoZ5phIB=<98QKX>jn-rv!NSkL$-+5;=Drwbt6|5XgLnTROs3 z_0A+i52M_`uVE`TBrdx$YR!@WNkXF&XPN|NBa_CBWZ&w4vJeC@rln18Em@kDUPL>P}P0ApMUB)Ko13Vn0& zkubIqVq2~tKhBE(i8Co7!zq%Iiz4b>%0n^leV$$b%nu*q`$XEg{LnAYaPq3~efcvF z(l0*tOxK@I4_w3j!7GkNJ$-@AtrQThm`BBjY+;oZXxGTf;O`NS{F>-hOsAZ2z%MqE zf9B`$uD_AuWl>CpC1++w!N*1lSM+ON_Nxy#J->Rz`8Eu>oM7(~Ym$r`QP#rRza5|$ zmv}9IE+=tO5c`D%awl$GwbwBJWe3$~SB_8w?Zo?Cy?)ai>6t4Ns_uDO^m!-43>>Q+V`yQ#duX_-Z=4BWWW0GOm12oOUTHOkA zzp|P%+LjwdhOX}g$%wNiq(YGg3FF39sv>Gz?;W6jh8%3t{pvsXlwpM6+pBQx^I!GW zs8%S#T;;Xe9dxd8;;N4boV{|iHJ{6q)UO(T{>xV}2>RLasZoSf-p@x}L~u$N2#a;S zwKl#%XUOF;DLae)g<{=e0X1?#PI6A_mYnDB+CDziFrM*E^?CTNVdCk9?Ck7cVpboa ztdOF6s@S>%%d?@>-_*d&8FY!)t4k2p#3vjdH#mmhO`?77@R;0UTj2J`GrMUN4!f@P^<&^4AohbAadpzRNQGk^0#4FMXum6GPOubVIW<%Akyu_|$~|;i z!)$gIqoD;nQrU*ibI-k8Lf1s5?$DK^yoQR7H@Nnx&BTTD&xrKh@ds_J1{aNanr6&o z)Z}gvvFH{}pE>WMZyduy-t@qap@i1V{{6l*t_}@B0C4tCk|3EK#0^e~Neofb3PfgD zc4!#kE3s*G78!2=po2!ij0XSdcXG~{Dz1T~&+fKvq3Ns~BiuWW*b+`w4Jj_`=}0XS z?5}tRPj;Ud{(R;@p5>?0wzAgpj$i$xLVWByDHTr~Y&EC2!Ghs8Ob2P^Pk(%7cvIBW zitf$%MwZXZidvB2Mt>JDz44(=^ri64MB=)kiXtVYMR_qfFhePxmb}#cQ+~4iVw_B3 z;LcBXX60zZMr_U6{Kz+ z^dmnZ=|TuD8dN7f>D-y!`&Pt~QShPEF)ZEGY5aDl(hjz;_syi)w401e`ZBK^qgQtP zY`KZK6vpaFW*p~Un7v;2ktk$OnNY7-bi9&tH$;ru&HDU8jY!yl=Zza~#t(07w_Iv= zChBSBj1S~}@%ZJcsgsWr@<)!_x;pY&b4I91*^ko4Js)Hke78 zn0K8|?%c|x!xMD?ip^wHbOaAs7-KhC<4J9}3H*`r;B2yvd#51V3O0dJ_QS1hya&(?UX!SnK5W508#U zJijeO_sHzAdM@--Kv55R7_1Z^+ZlG~CC2a(a0prV-ui9;Z&;d4r*Pm;9oXcRm7yh$ z&(dqk51OxYnSjIvKnfzIX}S0I%<&(;HF$~xCPo5@bUA(0y;@@pf6k20ofxitWLx)` zBU_6LKLA=R8^a6hsa-i`$G?|D$zRCqX>F#eZ{#;NFaXt*T*PB z0c^PPH-D`647t6w);7L1s!*G}WS_ojqUKXyKPw^PfniP3+H9P2q;c}Cy1%o#hOWC} zQX!hre0cpf*4FUn&7MCS!o1*dQ8rn1o$;U&x(9y=s2D!#F##2N{+Qkpreg$s08DS{ z|8mq Date: Thu, 5 Dec 2024 02:12:33 +0100 Subject: [PATCH 21/21] grid: Draw grid behind spine --- src/spine/scientific.typ | 8 +++++--- tests/plot/grid/ref/1.png | Bin 26822 -> 26301 bytes 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spine/scientific.typ b/src/spine/scientific.typ index 2c3b560..e1b3e82 100644 --- a/src/spine/scientific.typ +++ b/src/spine/scientific.typ @@ -69,9 +69,6 @@ draw.group(name: "spine", { for (ax, dir, grid-length, proj, style, mirror) in axes { - draw.on-layer(style.axis-layer, { - draw.line(proj(ax.min), proj(ax.max), stroke: style.stroke, mark: style.mark) - }) if "computed-ticks" in ax { if not mirror { grid.draw-cartesian(proj, 0, grid-length, dir, ax.computed-ticks, style.grid, ax.grid) @@ -79,6 +76,11 @@ ticks.draw-cartesian(proj, dir, ax.computed-ticks, style, is-mirror: mirror) } } + for (ax, dir, grid-length, proj, style, mirror) in axes { + draw.on-layer(style.axis-layer, { + draw.line(proj(ax.min), proj(ax.max), stroke: style.stroke, mark: style.mark) + }) + } }) let label-config = ( diff --git a/tests/plot/grid/ref/1.png b/tests/plot/grid/ref/1.png index 828459e42fb2d34d82bc5cd1a717622152ddad74..6e32948c581edc92d2e62e0293beaf010f39f2e0 100644 GIT binary patch delta 23448 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 V*&@Zs;QrRPaM-|HzntK7`CkP3?sNbE delta 24011 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 bb6SfYE4y(obAOC3Y(uB1u6-^aYaQ|*8%Z*a