diff --git a/doc/style.typ b/doc/style.typ index 95512a7..1698ccf 100644 --- a/doc/style.typ +++ b/doc/style.typ @@ -58,23 +58,50 @@ if type(types) != array { types = (types,) } - stack(dir: ttb, spacing: 1em, - // name Default: - block(breakable: false, width: 100%, stack(dir: ltr, - [#text(weight: "bold", name + [:]) #types.map(tidy.styles.default.show-type).join(" or ")], - if show-default { - align(right)[ - Default: #raw( - lang: "typc", - // Tidy gives defaults as strings but outside of tidy we pass defaults as the actual values - if in-tidy { default } else { repr(default) } - ) - ] - } - )), - // text - block(inset: (left: .4cm), content) + + block( + breakable: false, + above: 1em, below: 2em, spacing: 1em, + { + block({ + box(heading(name, level: 4)) + [:] + [#types.map(tidy.styles.default.show-type).join(" or ")] + h(1fr) + if show-default { + [Default: ] + raw( + lang: "typc", + // Tidy gives defaults as strings but outside of tidy we pass defaults as the actual values + if in-tidy { default } else { repr(default) } + ) + } + }) + block(inset: (left: .4cm), content) + } ) + + // block( + // breakable: false, + // above: 1em, below: 2em, spacing: 1em, + // stack( + // dir: ttb, spacing: 1em, + // // name Default: + // block(breakable: false, width: 100%, stack(dir: ltr, + // [#text(weight: "bold", box(heading(name, level: 4)) + [:]) #types.map(tidy.styles.default.show-type).join(" or ")], + // if show-default { + // align(right)[ + // Default: #raw( + // lang: "typc", + // // Tidy gives defaults as strings but outside of tidy we pass defaults as the actual values + // if in-tidy { default } else { repr(default) } + // ) + // ] + // } + // )), + // // text + // block(inset: (left: .4cm), content) + // )) } @@ -90,7 +117,7 @@ show-parameter-list: show-parameter-list ) -#let parse-show-module(path) = { +#let parse-show-module(path, ..args) = { tidy.show-module( tidy.parse-module( read(path), @@ -102,6 +129,7 @@ ), show-outline: false, sort-functions: none, - style: style + style: style, + ..args ) } diff --git a/doc/util.typ b/doc/util.typ index d0715c2..4933d9b 100644 --- a/doc/util.typ +++ b/doc/util.typ @@ -1,4 +1,5 @@ #import "/src/lib.typ" as cetz-plot +#import "/src/cetz.typ" /// Make the title-page #let make-title() = { @@ -33,23 +34,24 @@ set text(weight: "bold", left-color) show link: set text(left-color) - block( + block({ place( top + left, dx: -left-fringe * 22cm + 5mm, text(3cm, right-color)[CeTZ] - ) + + ) text(3cm)[Plot] - ) - block( - v(1cm) + + }) + + block({ + v(1cm) text( 20pt, authors.map(v => link(v.at(1), [#v.at(0)])).join("\n") ) - ) - block( - v(2cm) + + }) + block({ + v(2cm) text( 20pt, link( @@ -57,6 +59,44 @@ [Version ] + [#cetz-plot.version] ) ) - ) + }) + + block({ + v(2cm) + set text(fill: black) + cetz.canvas({ + cetz-plot.plot( + size: (8,5), + x-tick-step: calc.pi / 4, + x-minor-tick-step: calc.pi / 16, + x-grid: "both", + x-min: 0, x-max: 2 * calc.pi, + x-format: cetz-plot.axes.format.multiple-of, + + y-min: -1, y-max: 1, y-tick-step: 0.5, y-minor-tick-step: 0.1, + y-grid: "both", + { + cetz-plot.add.xy( + calc.sin, + domain: (0,2*calc.pi), + label: $y=x$, + line: "raw", + samples: 100, + epigraph: true, + ) + + cetz-plot.add.xy( + (t)=>calc.pow(calc.sin(t),2), + domain: (0, 2* calc.pi), + line: "raw", + samples: 100, + hypograph: true, + label: $sin^2 (x)$ + ) + } + ) + }) + }) + pagebreak(weak: true) } diff --git a/manual.old.typ b/manual.old.typ new file mode 100644 index 0000000..6a938ce --- /dev/null +++ b/manual.old.typ @@ -0,0 +1,66 @@ +#import "/doc/util.typ": * +#import "/doc/example.typ": example +#import "/doc/style.typ" as doc-style +#import "/src/lib.typ": * +#import "/src/cetz.typ": * +#import "@preview/tidy:0.2.0" + + +// Usage: +// ```example +// /* canvas drawing code */ +// ``` +#show raw.where(lang: "example"): example +#show raw.where(lang: "example-vertical"): example.with(vertical: true) + +#make-title() + +#set terms(indent: 1em) +#set par(justify: true) +#set heading(numbering: (..num) => if num.pos().len() < 4 { + numbering("1.1", ..num) + }) +#show link: set text(blue) + +// Outline +#{ + show heading: none + columns(2, outline(indent: true, depth: 3)) + pagebreak(weak: true) +} + +#set page(numbering: "1/1", header: align(right)[CeTZ-Plot]) + += Introduction + +CeTZ-Plot is a simple plotting library for use with CeTZ. + += Usage + +This is the minimal starting point: +#pad(left: 1em)[```typ +#import "@preview/cetz:0.2.2" +#import "@preview/cetz-plot:0.1.0" +#cetz.canvas({ + import cetz.draw: * + import cetz-plot: * + ... +}) +```] +Note that plot functions are imported inside the scope of the `canvas` block. +All following example code is expected to be inside a `canvas` block, with the `plot` +module imported into the namespace. + += Plot + +#doc-style.parse-show-module("/src/plot.typ") +#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin") { + doc-style.parse-show-module("/src/plot/" + m + ".typ") +} + += Chart + +#doc-style.parse-show-module("/src/chart.typ") +#for m in ("barchart", "boxwhisker", "columnchart", "piechart") { + doc-style.parse-show-module("/src/chart/" + m + ".typ") +} diff --git a/manual.pdf b/manual.pdf index f6623bc..24ff1b5 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index 6a938ce..6172eaa 100644 --- a/manual.typ +++ b/manual.typ @@ -25,42 +25,205 @@ // Outline #{ show heading: none - columns(2, outline(indent: true, depth: 3)) + outline(indent: true, depth: 3) pagebreak(weak: true) } #set page(numbering: "1/1", header: align(right)[CeTZ-Plot]) -= Introduction += Introduction -CeTZ-Plot is a simple plotting library for use with CeTZ. +CeTZ-Plot is a package for making plots in Typst using CeTZ. -= Usage += Usage This is the minimal starting point: #pad(left: 1em)[```typ #import "@preview/cetz:0.2.2" #import "@preview/cetz-plot:0.1.0" #cetz.canvas({ - import cetz.draw: * - import cetz-plot: * - ... + cetz-plot.plot(...,{ + + }) }) ```] -Note that plot functions are imported inside the scope of the `canvas` block. -All following example code is expected to be inside a `canvas` block, with the `plot` -module imported into the namespace. + +Note that plot functions are imported inside the scope of the `canvas` block. All following example code is expected to be inside a `canvas` block, with the `cetz-plot` module imported into the namespace. = Plot -#doc-style.parse-show-module("/src/plot.typ") -#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin") { - doc-style.parse-show-module("/src/plot/" + m + ".typ") -} +#doc-style.parse-show-module("/src/plot.typ", first-heading-level: 1) -= Chart +To draw elements insides a plot, using the plots coordinate system, use +the `plot.annotate(..)` function. -#doc-style.parse-show-module("/src/chart.typ") -#for m in ("barchart", "boxwhisker", "columnchart", "piechart") { - doc-style.parse-show-module("/src/chart/" + m + ".typ") -} +=== Options + +You can use the following options to customize each axis of the plot. You must pass them as named arguments prefixed by the axis name followed by a dash (`-`) they should target. Example: `x-min: 0`, `y-ticks: (..)` or `x2-label: [..]`. + +#doc-style.show-parameter-block("label", ("none", "content"), default: none)[ + The axis' label. If and where the label is drawn depends on the `axis-style`. + #example(``` + cetz-plot.plot( + size: (5,5), + x-label: [My $x$-label], + y-label: [Intensity [$"cts"$]], + { + cetz-plot.add.xy( + domain: (0, 2 * calc.pi), + t => (calc.cos(t), calc.sin(t)) + ) + } + ) + ```) +] + +#doc-style.show-parameter-block("min", ("auto", "float"), default: auto)[ + Axis lower domain value. If this is set greater than than `max`, the axis' direction is swapped + #example(``` + cetz-plot.plot( + size: (5,5), + x-min: -5, x-max: 5, + y-min: -2, + { + cetz-plot.add.xy( + domain: (0, 2 * calc.pi), + t => (calc.cos(t), calc.sin(t)) + ) + } + ) + ```) +] + +#doc-style.show-parameter-block("max", ("auto", "float"), default: auto)[ + Axis upper domain value. If this is set to a lower value than `min`, the axis' direction is swapped +] + +#doc-style.show-parameter-block("equal", ("string"), default: none)[ + Set the axis domain to keep a fixed aspect ratio by multiplying the other axis domain by the plots aspect ratio, + depending on the other axis orientation (see `horizontal`). + This can be useful to force one axis to grow or shrink with another one. + You can only "lock" two axes of different orientations. + #example(``` + cetz-plot.plot( + size: (5,2.5), + x-tick-step: 1, y-tick-step: 1, + x-equal: "y", + { + cetz-plot.add.xy( + domain: (0, 2 * calc.pi), + t => (calc.cos(t), calc.sin(t)) + ) + } + ) + ```) +] + +#doc-style.show-parameter-block("horizontal", ("bool"), default: "axis name dependant")[ + If true, the axis is considered an axis that gets drawn horizontally, vertically otherwise. + The default value depends on the axis name on axis creation. Axes which name start with `x` have this + set to `true`, all others have it set to `false`. Each plot has to use one horizontal and one + vertical axis for plotting, a combination of two y-axes will panic: ("y", "y2"). +] + +#doc-style.show-parameter-block("tick-step", ("none", "auto", "float"), default: auto)[ + The increment between tick marks on the axis. If set to `auto`, an + increment is determined. When set to `none`, incrementing tick marks are disabled. +] + +#doc-style.show-parameter-block("minor-tick-step", ("none", "float"), default: none)[ + Like `tick-step`, but for minor tick marks. In contrast to ticks, minor ticks do not have labels. +] + +#doc-style.show-parameter-block("ticks", ("none", "array"), default: none)[ + A List of custom tick marks to additionally draw along the axis. They can be passed as + an array of `` values or an array of `(, )` tuples for + setting custom tick mark labels per mark. + + #example(``` + cetz-plot.plot( + x-min: 0, x-max: 4, + x-tick-step: none, + x-ticks: (1, 2, 3), + + y-min: 1, y-max: 2, + y-tick-step: none, + y-ticks: ((1, [One]), (2, [Two])), + { + cetz-plot.add.xy(((0,0),)) + } + ) + ```) + + Examples: `(1, 2, 3)` or `((1, [One]), (2, [Two]), (3, [Three]))` +] + +#doc-style.show-parameter-block("format", ("none", "string", "function"), default: "float")[ + How to format the tick label: You can give a function that takes a `` and return + `` to use as the tick label. You can also give one of the predefined options: + / float: Floating point formatting rounded to two digits after the point (see `decimals`) + / sci: Scientific formatting with $times 10^n$ used as exponet syntax + + #example(``` + let formatter(v) = if v != 0 { + $ #{v/calc.pi} pi $ + } else { + $ 0 $ + } + + cetz-plot.plot( + x-tick-step: calc.pi, + x-min: 0, x-max: 2 * calc.pi, + x-format: formatter, + { + cetz-plot.add.xy(((0,0),)) + }) + ```) +] + +#doc-style.show-parameter-block("decimals", ("int"), default: 2, [ + Number of decimals digits to display for tick labels, if the format is set + to `"float"`. +]) + +#doc-style.show-parameter-block("unit", ("none", "content"), default: none)[ + Suffix to append to all tick labels. +] + +#doc-style.show-parameter-block("mode", ("none", "string"), default: none)[ + The scaling function of the axis. Takes `lin` (default) for linear scaling, + and `log` for logarithmic scaling. +] + +#doc-style.show-parameter-block("base", ("none", "number"), default: none)[ + The base to be used when labeling axis ticks in logarithmic scaling +] + +#doc-style.show-parameter-block("grid", ("bool", "string"), default: false)[ + If `true` or `"major"`, show grid lines for all major ticks. If set + to `"minor"`, show grid lines for minor ticks only. + The value `"both"` enables grid lines for both, major- and minor ticks. + + #example(``` + cetz-plot.plot( + x-min: 0, x-max: 2, x-grid: "both", + x-tick-step: 1, + + y-min: 0, y-max: 2, y-grid: "both", + y-tick-step: 1, y-minor-tick-step: .2, + { + cetz-plot.add.xy(((0,0),)) + + } + ) + ```) +] + +#doc-style.show-parameter-block("break", ("bool"), default: false)[ + If true, add a "sawtooth" at the start or end of the axis line, depending + on the axis bounds. If the axis min. value is > 0, a sawtooth is added + to the start of the axes, if the axis max. value is < 0, a sawtooth is added + to its end. +] + +// = Chart \ No newline at end of file diff --git a/src/axes.typ b/src/axes.typ deleted file mode 100644 index cfd4669..0000000 --- a/src/axes.typ +++ /dev/null @@ -1,924 +0,0 @@ -#import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process -#import "/src/plot/formats.typ" - -#let typst-content = content - -/// 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, - 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, -)) - -#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 -// - unit (content): Tick label suffix -// - decimals (int): Tick float decimal length -// - label (content): Axis label -// - mode (string): Axis scaling function. Takes `lin` or `log` -// - base (number): Base for tick labels when logarithmically scaled. -#let axis(min: -1, max: 1, label: none, - ticks: (step: auto, minor-step: none, - unit: none, decimals: 2, grid: false, - format: "float" - ), - mode: auto, base: auto) = ( - min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, mode: mode, base: base -) - -// Format a tick value -#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 } - - let round(value, digits) = { - calc.round(value, digits: digits) - } - - let format-float(value, digits) = { - $#round(value, digits)$ - } - - if type(value) != typst-content { - let format = tic-options.at("format", default: "float") - if format == none { - value = [] - } else if type(format) == typst-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 = format-float(value, tic-options.at("decimals", default: 2)) - } - } else if type(value) != typst-content { - value = str(value) - } - - if tic-options.at("unit", default: none) != none { - value += tic-options.unit - } - 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 intervall. -// 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 vector along a x, y and z axis -// -// - size (vector): Coordinate system size -// - x-axis (axis): X axis -// - y-axis (axis): Y axis -// - z-axis (axis): Z axis -// - vec (vector): Input vector to transform -// -> vector -#let transform-vec(size, x-axis, y-axis, z-axis, vec) = { - - let (x,y,) = for (dim, axis) in (x-axis, y-axis).enumerate() { - - let s = size.at(dim) - axis.inset.sum() - let o = 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 range = transform-func(axis.max) - transform-func(axis.min) - - let f = s / range - ((transform-func(vec.at(dim)) - transform-func(axis.min)) * f + o,) - } - - return (x, y, 0) -} - -// Draw inside viewport coordinates of two axes -// -// - size (vector): Axis canvas size (relative to origin) -// - x (axis): Horizontal axis -// - y (axis): Vertical axis -// - z (axis): Z axis -// - name (string,none): Group name -#let axis-viewport(size, x, y, z, 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(size, x, y, none, pt) - })) - }) - } - if "pos" in d { - d.pos = transform-vec(size, x, y, none, 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) == typst-content { - style.shared-zero - } else { - $0$ - } - content(pt, zero, anchor: "north-east") - } - }) - }) - }) -} diff --git a/src/axes/axes.typ b/src/axes/axes.typ new file mode 100644 index 0000000..58afcbe --- /dev/null +++ b/src/axes/axes.typ @@ -0,0 +1,72 @@ +#import "/src/cetz.typ": styles, util +#import "/src/plot/styles.typ": default-style, prepare-style, get-axis-style +#import "format.typ" + +// 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 +// - unit (content): Tick label suffix +// - decimals (int): Tick float decimal length +// - label (content): Axis label +// - mode (string): Axis scaling function. Takes `lin` or `log` +// - base (number): Base for tick labels when logarithmically scaled. +#let axis( + min: -1, + max: 1, + label: none, + ticks: ( + step: auto, + minor-step: none, + unit: none, + decimals: 2, + grid: false, + format: "float" + ), + mode: + auto, + base: auto +) = ( + min: min, + max: max, + ticks: ticks, + label: label, + inset: (0, 0), + show-break: false, + mode: mode, + base: base +) + +// Prepares the axis post creation. The given axis +// must be completely set-up, including its intervall. +// Returns the prepared axis +#let prepare-axis(ctx, axis, name) = { + let style = styles.resolve( + ctx.style, + root: "axes", + base: default-style + ) + 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 +} + +#import "ticks.typ" \ No newline at end of file diff --git a/src/plot/formats.typ b/src/axes/format.typ similarity index 96% rename from src/plot/formats.typ rename to src/axes/format.typ index c51dbc4..0a3bf2d 100644 --- a/src/plot/formats.typ +++ b/src/axes/format.typ @@ -42,7 +42,7 @@ /// /// ```example /// plot.plot(size: (5,1), -/// x-format: plot.formats.fraction, +/// x-format: axes.formats.fraction, /// x-tick-step: 1/5, /// y-tick-step: none, { /// plot.add(calc.sin, domain: (-1, 1)) @@ -63,7 +63,7 @@ /// /// ```example /// plot.plot(size: (5,1), -/// x-format: plot.formats.multiple-of, +/// x-format: axes.formats.multiple-of, /// x-tick-step: calc.pi/4, /// y-tick-step: none, { /// plot.add(calc.sin, domain: (-calc.pi, 1.5 * calc.pi)) @@ -107,7 +107,7 @@ /// /// ```example /// plot.plot(size: (5,1), -/// x-format: plot.formats.sci, +/// x-format: axes.formats.sci, /// x-tick-step: 1e3, /// y-tick-step: none, { /// plot.add(x => x, domain: (-2e3, 2e3)) @@ -136,4 +136,4 @@ return $#value times 10^#exponent$ } return $#value$ -} +} \ No newline at end of file diff --git a/src/axes/ticks.typ b/src/axes/ticks.typ new file mode 100644 index 0000000..0c797c3 --- /dev/null +++ b/src/axes/ticks.typ @@ -0,0 +1,307 @@ +#import "/src/cetz.typ": vector, util, draw +#import "format.typ" as formats + +// 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 } + + let round(value, digits) = { + calc.round(value, digits: digits) + } + + let format-float(value, digits) = { + $#round(value, digits)$ + } + + if type(value) != content { + let format = tic-options.at("format", default: "float") + if format == none { + value = [] + } else if type(format) == 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 = format-float(value, tic-options.at("decimals", default: 2)) + } + } else if type(value) != content { + value = str(value) + } + + if tic-options.at("unit", default: none) != none { + value += tic-options.unit + } + 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 +} + +// 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) + } + } +} \ No newline at end of file diff --git a/src/chart.typ b/src/chart.typ deleted file mode 100644 index 251cf65..0000000 --- a/src/chart.typ +++ /dev/null @@ -1,4 +0,0 @@ -#import "chart/boxwhisker.typ": boxwhisker, boxwhisker-default-style -#import "chart/barchart.typ": barchart, barchart-default-style -#import "chart/columnchart.typ": columnchart, columnchart-default-style -#import "chart/piechart.typ": piechart, piechart-default-style diff --git a/src/chart/barchart.typ b/src/chart/barchart.typ deleted file mode 100644 index ec7df40..0000000 --- a/src/chart/barchart.typ +++ /dev/null @@ -1,141 +0,0 @@ -#import "/src/cetz.typ": draw, styles, palette - -#import "/src/plot.typ" - -#let barchart-default-style = ( - axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))), - bar-width: .8, - cluster-gap: 0, - error: ( - whisker-size: .25, - ), - y-inset: 1, -) - -/// Draw a bar chart. A bar chart is a chart that represents data with -/// rectangular bars that grow from left to right, proportional to the values -/// they represent. -/// -/// = Styling -/// *Root*: `barchart`. -/// #show-parameter-block("bar-width", "float", default: .8, [ -/// Width of a single bar (basic) or a cluster of bars (clustered) in the plot.]) -/// #show-parameter-block("y-inset", "float", default: 1, [ -/// Distance of the plot data to the plot's edges on the y-axis of the plot.]) -/// You can use any `plot` or `axes` related style keys, too. -/// -/// The `barchart` function is a wrapper of the `plot` API. Arguments passed -/// to `..plot-args` are passed to the `plot.plot` function. -/// -/// - data (array): Array of data rows. A row can be of type array or -/// dictionary, with `label-key` and `value-key` being -/// the keys to access a rows label and value(s). -/// -/// *Example* -/// ```typc -/// (([A], 1), ([B], 2), ([C], 3),) -/// ``` -/// - label-key (int,string): Key to access the label of a data row. -/// This key is used as argument to the -/// rows `.at(..)` function. -/// - value-key (int,string): Key(s) to access values of a data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - error-key (none,int,string): Key(s) to access error values of a data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - mode (string): Chart mode: -/// / basic: Single bar per data row -/// / clustered: Group of bars per data row -/// / stacked: Stacked bars per data row -/// / stacked100: Stacked bars per data row relative -/// to the sum of the row -/// - size (array): Chart size as width and height tuple in canvas unist; -/// width can be set to `auto`. -/// - bar-style (style,function): Style or function (idx => style) to use for -/// each bar, accepts a palette function. -/// - x-unit (content,auto): Tick suffix added to each tick label -/// - y-label (content,none): Y axis label -/// - x-label (content,none): x axis label -/// - labels (none,content): Legend labels per x value group -/// - ..plot-args (any): Arguments to pass to `plot.plot` -#let barchart(data, - label-key: 0, - value-key: 1, - error-key: none, - mode: "basic", - size: (auto, 1), - bar-style: palette.red, - x-label: none, - x-unit: auto, - y-label: none, - labels: none, - ..plot-args - ) = { - assert(type(label-key) in (int, str)) - if mode == "basic" { - assert(type(value-key) in (int, str)) - } else { - assert(type(value-key) == array) - } - - if type(value-key) != array { - value-key = (value-key,) - } - - if error-key == none { - error-key = () - } else if type(error-key) != array { - error-key = (error-key,) - } - - if type(size) != array { - size = (size, auto) - } - if size.at(1) == auto { - size.at(1) = (data.len() + 1) - } - - let y-tic-list = data.enumerate().map(((i, t)) => { - (data.len() - i - 1, t.at(label-key)) - }) - - let x-unit = x-unit - if x-unit == auto { - x-unit = if mode == "stacked100" {[%]} else [] - } - - data = data.enumerate().map(((i, d)) => { - (data.len() - i - 1, value-key.map(k => d.at(k, default: 0)).flatten(), error-key.map(k => d.at(k, default: 0)).flatten()) - }) - - draw.group(ctx => { - let style = styles.resolve(ctx.style, merge: (:), - root: "barchart", base: barchart-default-style) - draw.set-style(..style) - - let y-inset = calc.max(style.y-inset, style.bar-width / 2) - plot.plot(size: size, - axis-style: "scientific-auto", - x-label: x-label, - x-grid: true, - y-label: y-label, - y-min: -y-inset, - y-max: data.len() + y-inset - 1, - y-tick-step: none, - y-ticks: y-tic-list, - plot-style: bar-style, - ..plot-args, - { - plot.add-bar(data, - x-key: 0, - y-key: 1, - error-key: if mode in ("basic", "clustered") { 2 }, - mode: mode, - labels: labels, - bar-width: -style.bar-width, - cluster-gap: style.cluster-gap, - axes: ("y", "x")) - }) - }) -} diff --git a/src/chart/barcol-common.typ b/src/chart/barcol-common.typ deleted file mode 100644 index 0c09a52..0000000 --- a/src/chart/barcol-common.typ +++ /dev/null @@ -1,40 +0,0 @@ -// Valid bar- and columnchart modes -#let barchart-modes = ( - "basic", "clustered", "stacked", "stacked100" -) - -// Functions for max value calculation -#let barchart-max-value-fn = ( - basic: (data, value-key) => { - calc.max(0, ..data.map(t => t.at(value-key))) - }, - clustered: (data, value-key) => { - calc.max(0, ..data.map(t => calc.max( - ..value-key.map(k => t.at(k))))) - }, - stacked: (data, value-key) => { - calc.max(0, ..data.map(t => - value-key.map(k => t.at(k)).sum())) - }, - stacked100: (..) => { - 100 - } -) - -// Functions for min value calculation -#let barchart-min-value-fn = ( - basic: (data, value-key) => { - calc.min(0, ..data.map(t => t.at(value-key))) - }, - clustered: (data, value-key) => { - calc.min(0, ..data.map(t => calc.max( - ..value-key.map(k => t.at(k))))) - }, - stacked: (data, value-key) => { - calc.min(0, ..data.map(t => - value-key.map(k => t.at(k)).sum())) - }, - stacked100: (..) => { - 0 - } -) diff --git a/src/chart/boxwhisker.typ b/src/chart/boxwhisker.typ deleted file mode 100644 index 1d643cf..0000000 --- a/src/chart/boxwhisker.typ +++ /dev/null @@ -1,97 +0,0 @@ -#import "/src/cetz.typ": draw, styles, palette, util, vector, intersection - -#import "/src/plot.typ" - -#let boxwhisker-default-style = ( - axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))), - box-width: 0.75, - whisker-width: 0.5, - mark-size: 0.15, -) - -/// Add one or more box or whisker plots. -/// -/// #example(``` -/// cetz.chart.boxwhisker(size: (2,2), label-key: none, -/// y-min: 0, y-max: 70, y-tick-step: 20, -/// (x: 1, min: 15, max: 60, -/// q1: 25, q2: 35, q3: 50)) -/// ```) -/// -/// = Styling -/// *Root* `boxwhisker` -/// #show-parameter-block("box-width", "float", default: .75, [ -/// The width of the box. Since boxes are placed 1 unit next to each other, -/// a width of $1$ would make neighbouring boxes touch.]) -/// #show-parameter-block("whisker-width", "float", default: .5, [ -/// The width of the whisker, that is the horizontal bar on the top and bottom -/// of the box.]) -/// #show-parameter-block("mark-size", "float", default: .15, [ -/// The scaling of the mark for the boxes outlier values in canvas units.]) -/// You can use any `plot` or `axes` related style keys, too. -/// -/// - data (array, dictionary): Dictionary or array of dictionaries containing the -/// needed entries to plot box and whisker plot. -/// -/// See `plot.add-boxwhisker` for more details. -/// -/// *Examples:* -/// - ```typc -/// (x: 1 // Location on x-axis -/// outliers: (7, 65, 69), // Optional outliers -/// min: 15, max: 60 // Minimum and maximum -/// q1: 25, // Quartiles: Lower -/// q2: 35, // Median -/// q3: 50) // Upper -/// ``` -/// - size (array) : Size of chart. If the second entry is auto, it automatically scales to accommodate the number of entries plotted -/// - label-key (integer, string): Index in the array where labels of each entry is stored -/// - mark (string): Mark to use for plotting outliers. Set `none` to disable. Defaults to "x" -/// - ..plot-args (any): Additional arguments are passed to `plot.plot` -#let boxwhisker(data, - size: (1, auto), - label-key: 0, - mark: "*", - ..plot-args - ) = { - if type(data) == dictionary { data = (data,) } - - if type(size) != array { - size = (size, auto) - } - if size.at(1) == auto { - size.at(1) = (data.len() + 1) - } - - let x-tick-list = data.enumerate().map(((i, t)) => { - (i + 1, if label-key != none { t.at(label-key, default: i) } else { [] }) - }) - - draw.group(ctx => { - let style = styles.resolve(ctx.style, merge: (:), - root: "boxwhisker", base: boxwhisker-default-style) - draw.set-style(..style) - - plot.plot( - size: size, - axis-style: "scientific-auto", - x-tick-step: none, - x-ticks: x-tick-list, - y-grid: true, - x-label: none, - y-label: none, - ..plot-args, - { - for (i, row) in data.enumerate() { - plot.add-boxwhisker( - (x: i + 1, ..row), - box-width: style.box-width, - whisker-width: style.whisker-width, - style: (:), - mark: mark, - mark-size: style.mark-size - ) - } - }) - }) -} diff --git a/src/chart/columnchart.typ b/src/chart/columnchart.typ deleted file mode 100644 index 5a4fcb3..0000000 --- a/src/chart/columnchart.typ +++ /dev/null @@ -1,141 +0,0 @@ -#import "/src/cetz.typ": draw, styles, palette, util, vector, intersection - -#import "/src/plot.typ" - -#let columnchart-default-style = ( - axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))), - bar-width: .8, - cluster-gap: 0, - error: ( - whisker-size: .25, - ), - x-inset: 1, -) - -/// Draw a column chart. A column chart is a chart that represents data with -/// rectangular bars that grow from bottom to top, proportional to the values -/// they represent. -/// -/// = Styling -/// *Root*: `columnchart`. -/// #show-parameter-block("bar-width", "float", default: .8, [ -/// Width of a single bar (basic) or a cluster of bars (clustered) in the plot.]) -/// #show-parameter-block("x-inset", "float", default: 1, [ -/// Distance of the plot data to the plot's edges on the x-axis of the plot.]) -/// You can use any `plot` or `axes` related style keys, too. -/// -/// The `columnchart` function is a wrapper of the `plot` API. Arguments passed -/// to `..plot-args` are passed to the `plot.plot` function. -/// -/// - data (array): Array of data rows. A row can be of type array or -/// dictionary, with `label-key` and `value-key` being -/// the keys to access a rows label and value(s). -/// -/// *Example* -/// ```typc -/// (([A], 1), ([B], 2), ([C], 3),) -/// ``` -/// - label-key (int,string): Key to access the label of a data row. -/// This key is used as argument to the -/// rows `.at(..)` function. -/// - value-key (int,string): Key(s) to access value(s) of data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - error-key (none,int,string): Key(s) to access error values of a data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - mode (string): Chart mode: -/// / basic: Single bar per data row -/// / clustered: Group of bars per data row -/// / stacked: Stacked bars per data row -/// / stacked100: Stacked bars per data row relative -/// to the sum of the row -/// - size (array): Chart size as width and height tuple in canvas unist; -/// width can be set to `auto`. -/// - bar-style (style,function): Style or function (idx => style) to use for -/// each bar, accepts a palette function. -/// - y-unit (content,auto): Tick suffix added to each tick label -/// - y-label (content,none): Y axis label -/// - x-label (content,none): x axis label -/// - labels (none,content): Legend labels per y value group -/// - ..plot-args (any): Arguments to pass to `plot.plot` -#let columnchart(data, - label-key: 0, - value-key: 1, - error-key: none, - mode: "basic", - size: (auto, 1), - bar-style: palette.red, - x-label: none, - y-unit: auto, - y-label: none, - labels: none, - ..plot-args - ) = { - assert(type(label-key) in (int, str)) - if mode == "basic" { - assert(type(value-key) in (int, str)) - } - - if type(value-key) != array { - value-key = (value-key,) - } - - if error-key == none { - error-key = () - } else if type(error-key) != array { - error-key = (error-key,) - } - - if type(size) != array { - size = (auto, size) - } - if size.at(0) == auto { - size.at(0) = (data.len() + 1) - } - - let x-tic-list = data.enumerate().map(((i, t)) => { - (i, t.at(label-key)) - }) - - let y-unit = y-unit - if y-unit == auto { - y-unit = if mode == "stacked100" {[%]} else [] - } - - data = data.enumerate().map(((i, d)) => { - (i, value-key.map(k => d.at(k)).flatten(), error-key.map(k => d.at(k, default: 0)).flatten()) - }) - - draw.group(ctx => { - let style = styles.resolve(ctx.style, merge: (:), - root: "columnchart", base: columnchart-default-style) - draw.set-style(..style) - - let x-inset = calc.max(style.x-inset, style.bar-width / 2) - plot.plot(size: size, - axis-style: "scientific-auto", - y-grid: true, - y-label: y-label, - x-min: -x-inset, - x-max: data.len() + x-inset - 1, - x-tick-step: none, - x-ticks: x-tic-list, - x-label: x-label, - plot-style: bar-style, - ..plot-args, - { - plot.add-bar(data, - x-key: 0, - y-key: 1, - error-key: if mode in ("basic", "clustered") { 2 }, - mode: mode, - labels: labels, - bar-width: style.bar-width, - cluster-gap: style.cluster-gap, - error-style: style.error, - whisker-size: style.error.whisker-size, - axes: ("x", "y")) - }) - }) -} diff --git a/src/chart/piechart.typ b/src/chart/piechart.typ deleted file mode 100644 index dc4a526..0000000 --- a/src/chart/piechart.typ +++ /dev/null @@ -1,492 +0,0 @@ -#import "/src/cetz.typ": draw, styles, palette, util, vector, intersection -#import util: circle-arclen - -#import "/src/plot/legend.typ" - -// Piechart Label Kind -#let label-kind = (value: "VALUE", percentage: "%", label: "LABEL") - -// Piechart Default Style -#let default-style = ( - stroke: auto, - fill: auto, - /// Outer chart radius - radius: 1, - /// Inner slice radius - inner-radius: 0, - /// Gap between items. This can be a canvas length or an angle - gap: 0.5deg, - /// Outset offset, absolute or relative to radius - outset-offset: 10%, - /// Pie outset mode: - /// - "OFFSET": Offset slice position by outset-offset - /// - "RADIUS": Offset slice radius by outset-offset (the slice gets scaled) - outset-mode: "OFFSET", - /// Pie start angle - start: 90deg, - /// Pie stop angle - stop: 360deg + 90deg, - /// Pie rotation direction (true = clockwise, false = anti-clockwise) - clockwise: true, - outer-label: ( - /// Label kind - /// If set to a function, that function gets called with (value, label) of each item - content: label-kind.label, - /// Absolute radius or percentage of radius - radius: 125%, - /// Absolute angle or auto to use secant of the slice as direction - angle: 0deg, - /// Label anchor - anchor: "center", - ), - inner-label: ( - /// Label kind - /// If set to a function, that function gets called with (value, label) of each item - content: none, - /// Absolute radius or percentage of the mid between radius and inner-radius - radius: 150%, - /// Absolute angle or auto to use secant of the slice as direction - angle: 0deg, - /// Label anchor - anchor: "center", - ), - legend: ( - ..legend.default-style, - - /// Label used for the legend - /// The legend gets rendered as soon as at least one item with a label - /// exists and the `legend-label.content` is set != none. This field - /// accepts the same values as inner-label.content or outer-label.content. - label: "LABEL", - - /// Anchor of the charts data bounding box to place the legend relative to - position: "south", - - /// Anchor of the legend bounding box to use as origin - anchor: "north", - - /// Custom preview function override - /// The function takes an item dictionary an is responsible for drawing - /// the preview icon. Stroke and fill styles are set to match the items - /// style. - preview: none, - - /// See lenged.typ for the following style keys - orientation: ltr, - offset: (0,-.5em), - stroke: none, - item: ( - spacing: .25, - preview: ( - width: .3, - height: .3, - ), - ), - ) -) -#let piechart-default-style = default-style - - -/// Draw a pie- or donut-chart -/// -/// #example(``` -/// import cetz.chart -/// let data = (24, 31, 18, 21, 23, 18, 27, 17, 26, 13) -/// let colors = gradient.linear(red, blue, green, yellow) -/// -/// chart.piechart( -/// data, -/// radius: 1.5, -/// slice-style: colors, -/// inner-radius: .5, -/// outer-label: (content: "%",)) -/// ```) -/// -/// = Styling -/// *Root* `piechart` \ -/// #show-parameter-block("radius", ("number"), [ -/// Outer radius of the chart.], default: 1) -/// #show-parameter-block("inner-radius", ("number"), [ -/// Inner radius of the chart slices. If greater than zero, the chart becomes -/// a "donut-chart".], default: 0) -/// #show-parameter-block("gap", ("number", "angle"), [ -/// Gap between chart slices to leave empty. This does not increase the charts -/// radius by pushing slices outwards, but instead shrinks the slice. Big -/// values can result in slices becoming invisible if no space is left.], default: 0.5deg) -/// #show-parameter-block("outset-offset", ("number", "ratio"), [ -/// Absolute, or radius relative distance to push slices marked for -/// "outsetting" outwards from the center of the chart.], default: 10%) -/// #show-parameter-block("outset-offset", ("string"), [ -/// The mode of how to perform "outsetting" of slices: -/// - "OFFSET": Offset slice position by `outset-offset`, increasing their gap to their siblings -/// - "RADIUS": Offset slice radius by `outset-offset`, which scales the slice and leaves the gap unchanged], default: "OFFSET") -/// #show-parameter-block("start", ("angle"), [ -/// The pie-charts start angle (ccw). You can use this to draw charts not forming a full circle.], default: 90deg) -/// #show-parameter-block("stop", ("angle"), [ -/// The pie-charts stop angle (ccw).], default: 360deg + 90deg) -/// #show-parameter-block("clockwise", ("bool"), [ -/// The pie-charts rotation direction.], default: true) -/// #show-parameter-block("outer-label.content", ("none","string","function"), [ -/// Content to display outsides the charts slices. -/// There are the following predefined values: -/// / LABEL: Display the slices label (see `label-key`) -/// / %: Display the percentage of the items value in relation to the sum of -/// all values, rounded to the next integer -/// / VALUE: Display the slices value -/// If passed a `` of the format `(value, label) => content`, -/// that function gets called with each slices value and label and must return -/// content, that gets displayed.], default: "LABEL") -/// #show-parameter-block("outer-label.radius", ("number","ratio"), [ -/// Absolute, or radius relative distance from the charts center to position -/// outer labels at.], default: 125%) -/// #show-parameter-block("outer-label.angle", ("angle","auto"), [ -/// The angle of the outer label. If passed `auto`, the label gets rotated, -/// so that the baseline is parallel to the slices secant. ], default: 0deg) -/// #show-parameter-block("outer-label.anchor", ("string"), [ -/// The anchor of the outer label to use for positioning.], default: "center") -/// #show-parameter-block("inner-label.content", ("none","string","function"), [ -/// Content to display insides the charts slices. -/// See `outer-label.content` for the possible values.], default: none) -/// #show-parameter-block("inner-label.radius", ("number","ratio"), [ -/// Distance of the inner label to the charts center. If passed a ``, -/// that ratio is relative to the mid between the inner and outer radius (`inner-radius` and `radius`) -/// of the chart], default: 150%) -/// #show-parameter-block("inner-label.angle", ("angle","auto"), [ -/// See `outer-label.angle`.], default: 0deg) -/// #show-parameter-block("inner-label.anchor", ("string"), [ -/// See `outer-label.anchor`.], default: "center") -/// #show-parameter-block("legend.label", ("none","string","function"), [ -/// See `outer-label.content`. The legend gets shown if this key is set != none.], default: "LABEL") -/// -/// = Anchors -/// The chart places one anchor per item at the radius of it's slice that -/// gets named `"item-"` (outer radius) and `"item--inner"` (inner radius), -/// where index is the index of the sclice data in `data`. -/// -/// - data (array): Array of data items. A data item can be: -/// - A number: A number that is used as the fraction of the slice -/// - An array: An array which is read depending on value-key, label-key and outset-key -/// - A dictionary: A dictionary which is read depending on value-key, label-key and outset-key -/// - value-key (none,int,string): Key of the "value" of a data item. If for example -/// data items are passed as dictionaries, the value-key is the key of the dictionary to -/// access the items chart value. -/// - label-key (none,int,string): Same as the value-key but for getting an items label content. -/// - outset-key (none,int,string): Same as the value-key but for getting if an item should get outset (highlighted). The -/// outset can be a bool, float or ratio. If of type `bool`, the outset distance from the -/// style gets used. -/// - outset (none,int,array): A single or multiple indices of items that should get offset from the center to the outsides -/// of the chart. Only used if outset-key is none! -/// - slice-style (function,array,gradient): Slice style of the following types: -/// - function: A function of the form `index => style` that must return a style dictionary. -/// This can be a `palette` function. -/// - array: An array of style dictionaries or fill colors of at least one item. For each slice the style at the slices -/// index modulo the arrays length gets used. -/// - gradient: A gradient that gets sampled for each data item using the the slices -/// index divided by the number of slices as position on the gradient. -/// If one of stroke or fill is not in the style dictionary, it is taken from the charts style. -#let piechart(data, - value-key: none, - label-key: none, - outset-key: none, - outset: none, - slice-style: palette.red, - name: none, - ..style) = { - import draw: * - - // Prepare data by converting it to tuples of the format - // (value, label, outset) - data = data.enumerate().map(((i, item)) => ( - if value-key != none { - item.at(value-key) - } else { - item - }, - if label-key != none { - item.at(label-key) - } else { - none - }, - if outset-key != none { - item.at(outset-key, default: false) - } else if outset != none { - i == outset or (type(outset) == array and i in outset) - } else { - false - } - )) - - let sum = data.map(((value, ..)) => value).sum() - if sum == 0 { - sum = 1 - } - - group(name: name, ctx => { - anchor("default", (0,0)) - - let style = styles.resolve(ctx, - merge: style.named(), root: "piechart", base: default-style) - - let gap = style.gap - if type(gap) != angle { - gap = gap / (2 * calc.pi * style.radius) * 360deg - } - assert(gap < 360deg / data.len(), - message: "Gap angle is too big for " + str(data.len()) + "items. Maximum gap angle: " + repr(360deg / data.len())) - - let radius = style.radius - assert(radius > 0, - message: "Radius must be > 0.") - - let inner-radius = style.inner-radius - assert(inner-radius >= 0 and inner-radius <= radius, - message: "Radius must be >= 0 and <= radius.") - - assert(style.outset-mode in ("OFFSET", "RADIUS"), - message: "Outset mode must be 'OFFSET' or 'RADIUS', but is: " + str(style.outset-mode)) - - let style-at = if type(slice-style) == function { - slice-style - } else if type(slice-style) == array { - i => { - let s = slice-style.at(calc.rem(i, slice-style.len())) - if type(s) == color { - (fill: s) - } else { - s - } - } - } else if type(slice-style) == gradient { - i => (fill: slice-style.sample(i / data.len() * 100%)) - } - - let start-angle = style.start - let stop-angle = style.stop - let f = (stop-angle - start-angle) / sum - - let get-item-label(item, kind) = { - let (value, label, ..) = item - if kind == label-kind.value { - [#value] - } else if kind == label-kind.percentage { - [#{calc.round(value / sum * 100)}%] - } else if kind == label-kind.label { - label - } else if type(kind) == function { - (kind)(value, label) - } - } - - let start = start-angle - let enum-items = if style.clockwise { - data.enumerate().rev() - } else { - data.enumerate() - } - group(name: "chart", { - for (i, item) in enum-items { - let (value, label, outset) = item - if value == 0 { continue } - - let origin = (0,0) - let radius = radius - let inner-radius = inner-radius - - // Calculate item angles - let delta = f * value - let end = start + delta - - // Apply item outset - let outset-offset = if outset == true { - style.outset-offset - } else if outset == false { - 0 - } else if type(outset) in (float, ratio) { - outset - } else { - panic("Invalid type for outset. Expected bool, float or ratio, got: " + repr(outset)) - } - if type(outset-offset) == ratio { - outset-offset = outset-offset * radius / 100% - } - - if outset-offset != 0 { - if style.outset-mode == "OFFSET" { - let dir = (calc.cos((start + end) / 2), calc.sin((start + end) / 2)) - origin = vector.add(origin, vector.scale(dir, outset-offset)) - radius += outset-offset - } else { - radius += outset-offset - if inner-radius > 0 { - inner-radius += outset-offset - } - } - } - - // Calculate gap angles - let outer-gap = gap - let gap-dist = outer-gap / 360deg * 2 * calc.pi * radius - let inner-gap = if inner-radius > 0 { - gap-dist / (2 * calc.pi * inner-radius) * 360deg - } else { - 1 / calc.pi * 360deg - } - - // Calculate angle deltas - let outer-angle = end - start - outer-gap * 2 - let inner-angle = end - start - inner-gap * 2 - let mid-angle = (start + end) / 2 - - // Skip negative values - if outer-angle < 0deg { - // TODO: Add a warning as soon as Typst is ready! - continue - } - - // A sharp item is an item that should be round but is sharp due to the gap being big - let is-sharp = inner-radius == 0 or circle-arclen(inner-radius, angle: inner-angle) > circle-arclen(radius, angle: outer-angle) - - let inner-origin = vector.add(origin, if inner-radius == 0 { - if gap-dist >= 0 { - let outer-end = vector.scale((calc.cos(end - outer-gap), calc.sin(end - outer-gap)), radius) - let inner-end = vector.scale((calc.cos(end - inner-gap), calc.sin(end - inner-gap)), gap-dist) - let outer-start = vector.scale((calc.cos(start + outer-gap), calc.sin(start + outer-gap)), radius) - let inner-start = vector.scale((calc.cos(start + inner-gap), calc.sin(start + inner-gap)), gap-dist) - - intersection.line-line(outer-end, inner-end, outer-start, inner-start, ray: true) - } else { - (0,0) - } - } else if is-sharp { - let outer-end = vector.scale((calc.cos(end - outer-gap), calc.sin(end - outer-gap)), radius) - let inner-end = vector.scale((calc.cos(end - inner-gap), calc.sin(end - inner-gap)), inner-radius) - let outer-start = vector.scale((calc.cos(start + outer-gap), calc.sin(start + outer-gap)), radius) - let inner-start = vector.scale((calc.cos(start + inner-gap), calc.sin(start + inner-gap)), inner-radius) - - intersection.line-line(outer-end, inner-end, outer-start, inner-start, ray: true) - } else { - (0,0) - }) - - // Draw one segment - let stroke = style-at(i).at("stroke", default: style.stroke) - let fill = style-at(i).at("fill", default: style.fill) - if data.len() == 1 { - // If the chart has only one segment, we may have to fake a path - // with a hole in it by using a combination of multiple arcs. - if inner-radius > 0 { - // Split the circle/arc into two arcs - // and fill them - merge-path({ - arc(origin, start: start-angle, stop: mid-angle, radius: radius, anchor: "origin") - arc(origin, stop: start-angle, start: mid-angle, radius: inner-radius, anchor: "origin") - }, close: false, fill:fill, stroke: none) - merge-path({ - arc(origin, start: mid-angle, stop: stop-angle, radius: radius, anchor: "origin") - arc(origin, stop: mid-angle, start: stop-angle, radius: inner-radius, anchor: "origin") - }, close: false, fill:fill, stroke: none) - - // Create arcs for the inner and outer border and stroke them. - // If the chart is not a full circle, we have to merge two arc - // at their ends to create closing lines - if stroke != none { - if calc.abs(stop-angle - start-angle) != 360deg { - merge-path({ - arc(origin, start: start, stop: end, radius: inner-radius, anchor: "origin") - arc(origin, start: end, stop: start, radius: radius, anchor: "origin") - }, close: true, fill: none, stroke: stroke) - } else { - arc(origin, start: start, stop: end, radius: inner-radius, fill: none, stroke: stroke, anchor: "origin") - arc(origin, start: start, stop: end, radius: radius, fill: none, stroke: stroke, anchor: "origin") - } - } - } else { - arc(origin, start: start, stop: end, radius: radius, fill: fill, stroke: stroke, mode: "PIE", anchor: "origin") - } - } else { - // Draw a normal segment - if inner-origin != none { - merge-path({ - arc(origin, start: start + outer-gap, stop: end - outer-gap, anchor: "origin", - radius: radius) - if inner-radius > 0 and not is-sharp { - if inner-angle < 0deg { - arc(inner-origin, stop: end - inner-gap, delta: inner-angle, anchor: "origin", - radius: inner-radius) - } else { - arc(inner-origin, start: end - inner-gap, delta: -inner-angle, anchor: "origin", - radius: inner-radius) - } - } else { - line((rel: (end - outer-gap, radius), to: origin), - inner-origin, - (rel: (start + outer-gap, radius), to: origin)) - } - }, close: true, fill: fill, stroke: stroke) - } - } - - // Place outer label - let outer-label = get-item-label(item, style.outer-label.content) - if outer-label != none { - let r = style.outer-label.radius - if type(r) == ratio {r = r * radius / 100%} - - let dir = (r * calc.cos(mid-angle), r * calc.sin(mid-angle)) - let pt = vector.add(origin, dir) - - let angle = style.outer-label.angle - if angle == auto { - angle = vector.add(pt, (dir.at(1), -dir.at(0))) - } - - content(pt, outer-label, angle: angle, anchor: style.outer-label.anchor) - } - - // Place inner label - let inner-label = get-item-label(item, style.inner-label.content) - if inner-label != none { - let r = style.inner-label.radius - if type(r) == ratio {r = r * (radius + inner-radius) / 200%} - - let dir = (r * calc.cos(mid-angle), r * calc.sin(mid-angle)) - let pt = vector.add(origin, dir) - - let angle = style.inner-label.angle - if angle == auto { - angle = vector.add(pt, (dir.at(1), -dir.at(0))) - } - - content(pt, inner-label, angle: angle, anchor: style.inner-label.anchor) - } - - // Place item anchor - anchor("item-" + str(i), (rel: (mid-angle, radius), to: origin)) - anchor("item-" + str(i) + "-inner", (rel: (mid-angle, inner-radius), to: origin)) - - start = end - } - }) - - legend.legend((name: "chart", anchor: style.legend.position), { - let preview-fn = if style.legend.preview != none { - style.legend.preview - } else { - (_) => { rect((0,0), (1,1)) } - } - - for (i, item) in enum-items.rev() { - let label = get-item-label(item, style.legend.label) - let preview = (item) => { - let stroke = style-at(i).at("stroke", default: style.stroke) - let fill = style-at(i).at("fill", default: style.fill) - - set-style(stroke: stroke, fill: fill) - preview-fn(item) - } - - legend.item(label, preview) - } - }, ..style.at("legend", default: (:))) - }) -} diff --git a/src/lib.typ b/src/lib.typ index 128ff08..e6e9c7d 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,5 +1,7 @@ #let version = version(0,1,0) -#import "/src/axes.typ" -#import "/src/plot.typ" -#import "/src/chart.typ" +#import "/src/axes/axes.typ" +#import "/src/plot.typ": plot +#import "/src/plot/axis-style.typ" +#import "/src/plot/add.typ" + diff --git a/src/plot.typ b/src/plot.typ index 05640d5..94f6dad 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -1,271 +1,132 @@ -#import "/src/cetz.typ": util, draw, matrix, vector, styles, palette -#import util: bezier - -#import "/src/axes.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 -#import "/src/plot/boxwhisker.typ": add-boxwhisker -#import "/src/plot/util.typ" as plot-util -#import "/src/plot/legend.typ" as plot-legend -#import "/src/plot/annotation.typ": annotate, calc-annotation-domain -#import "/src/plot/bar.typ": add-bar -#import "/src/plot/errorbar.typ": add-errorbar -#import "/src/plot/mark.typ" -#import "/src/plot/violin.typ": add-violin -#import "/src/plot/formats.typ" -#import plot-legend: add-legend - -#let default-colors = (blue, red, green, yellow, black) +#import "/src/cetz.typ": draw, util, styles + +#import "plot/elements/annotation.typ": calc-annotation-domain +#import "plot/legend.typ" as plot-legend +#import "plot/axis-style.typ" +#import "plot/mark.typ" +#import "axes/axes.typ" + +// TODO: Refactor this into a better way of providing palettes + +#let default-colors = ( + rgb("#1982c4"), + rgb("#ff595e"), + rgb("#ffca3a"), + rgb("#8ac926"), + rgb("#6a4c93") +) #let default-plot-style(i) = { let color = default-colors.at(calc.rem(i, default-colors.len())) - return (stroke: color, - fill: color.lighten(75%)) + return ( + stroke: color, + fill: color.transparentize(80%) + ) } #let default-mark-style(i) = { return default-plot-style(i) } -/// 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:`. -/// -/// #example(``` -/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// plot.add(((0,0), (1,1), (2,.5), (4,3))) -/// }) -/// ```) -/// -/// To draw elements insides a plot, using the plots coordinate system, use -/// the `plot.annotate(..)` function. -/// -/// = parameters -/// -/// = Options -/// -/// You can use the following options to customize each axis of the plot. You must pass them as named arguments prefixed by the axis name followed by a dash (`-`) they should target. Example: `x-min: 0`, `y-ticks: (..)` or `x2-label: [..]`. -/// -/// #show-parameter-block("label", ("none", "content"), default: "none", [ -/// The axis' label. If and where the label is drawn depends on the `axis-style`.]) -/// #show-parameter-block("min", ("auto", "float"), default: "auto", [ -/// Axis lower domain value. If this is set greater than than `max`, the axis' direction is swapped]) -/// #show-parameter-block("max", ("auto", "float"), default: "auto", [ -/// Axis upper domain value. If this is set to a lower value than `min`, the axis' direction is swapped]) -/// #show-parameter-block("equal", ("string"), default: "none", [ -/// Set the axis domain to keep a fixed aspect ratio by multiplying the other axis domain by the plots aspect ratio, -/// depending on the other axis orientation (see `horizontal`). -/// This can be useful to force one axis to grow or shrink with another one. -/// You can only "lock" two axes of different orientations. -/// #example(``` -/// plot.plot(size: (2,1), x-tick-step: 1, y-tick-step: 1, -/// x-equal: "y", -/// { -/// plot.add(domain: (0, 2 * calc.pi), -/// t => (calc.cos(t), calc.sin(t))) -/// }) -/// ```) -/// ]) -/// #show-parameter-block("horizontal", ("bool"), default: "axis name dependant", [ -/// If true, the axis is considered an axis that gets drawn horizontally, vertically otherwise. -/// The default value depends on the axis name on axis creation. Axes which name start with `x` have this -/// set to `true`, all others have it set to `false`. Each plot has to use one horizontal and one -/// vertical axis for plotting, a combination of two y-axes will panic: ("y", "y2"). -/// ]) -/// #show-parameter-block("tick-step", ("none", "auto", "float"), default: "auto", [ -/// The increment between tick marks on the axis. If set to `auto`, an -/// increment is determined. When set to `none`, incrementing tick marks are disabled.]) -/// #show-parameter-block("minor-tick-step", ("none", "float"), default: "none", [ -/// Like `tick-step`, but for minor tick marks. In contrast to ticks, minor ticks do not have labels.]) -/// #show-parameter-block("ticks", ("none", "array"), default: "none", [ -/// A List of custom tick marks to additionally draw along the axis. They can be passed as -/// an array of `` values or an array of `(, )` tuples for -/// setting custom tick mark labels per mark. -/// -/// #example(``` -/// plot.plot(x-tick-step: none, y-tick-step: none, -/// x-min: 0, x-max: 4, -/// x-ticks: (1, 2, 3), -/// y-min: 1, y-max: 2, -/// y-ticks: ((1, [One]), (2, [Two])), -/// { -/// plot.add(((0,0),)) -/// }) -/// ```) -/// -/// Examples: `(1, 2, 3)` or `((1, [One]), (2, [Two]), (3, [Three]))`]) -/// #show-parameter-block("format", ("none", "string", "function"), default: "float", [ -/// How to format the tick label: You can give a function that takes a `` and return -/// `` to use as the tick label. You can also give one of the predefined options: -/// / float: Floating point formatting rounded to two digits after the point (see `decimals`) -/// / sci: Scientific formatting with $times 10^n$ used as exponet syntax -/// -/// #example(``` -/// let formatter(v) = if v != 0 {$ #{v/calc.pi} pi $} else {$ 0 $} -/// plot.plot(x-tick-step: calc.pi, y-tick-step: none, -/// x-min: 0, x-max: 2 * calc.pi, -/// x-format: formatter, -/// { -/// plot.add(((0,0),)) -/// }) -/// ```) -/// ]) -/// #show-parameter-block("decimals", ("int"), default: "2", [ -/// Number of decimals digits to display for tick labels, if the format is set -/// to `"float"`. -/// ]) -/// #show-parameter-block("unit", ("none", "content"), default: "none", [ -/// Suffix to append to all tick labels. -/// ]) -/// #show-parameter-block("mode", ("none", "string"), default: "none", [ -/// The scaling function of the axis. Takes `lin` (default) for linear scaling, -/// and `log` for logarithmic scaling.]) -/// #show-parameter-block("base", ("none", "number"), default: "none", [ -/// The base to be used when labeling axis ticks in logarithmic scaling]) -/// #show-parameter-block("grid", ("bool", "string"), default: "false", [ -/// If `true` or `"major"`, show grid lines for all major ticks. If set -/// to `"minor"`, show grid lines for minor ticks only. -/// The value `"both"` enables grid lines for both, major- and minor ticks. -/// -/// #example(``` -/// plot.plot(x-tick-step: 1, y-tick-step: 1, -/// y-minor-tick-step: .2, -/// x-min: 0, x-max: 2, x-grid: true, -/// y-min: 0, y-max: 2, y-grid: "both", { -/// plot.add(((0,0),)) -/// }) -/// ```) -/// ]) -/// #show-parameter-block("break", ("bool"), default: "false", [ -/// If true, add a "sawtooth" at the start or end of the axis line, depending -/// on the axis bounds. If the axis min. value is > 0, a sawtooth is added -/// to the start of the axes, if the axis max. value is < 0, a sawtooth is added -/// to its end.]) -/// -/// - body (body): Calls of `plot.add` or `plot.add-*` commands. Note that normal drawing -/// commands like `line` or `rect` are not allowed inside the plots body, instead wrap -/// them in `plot.annotate`, which lets you select the axes used for drawing. -/// - size (array): Plot size tuple of `(, )` in canvas units. -/// This is the plots inner plotting size without axes and labels. -/// - axis-style (none, string): How the axes should be styled: -/// / scientific: Frames plot area using a rectangle and draw axes `x` (bottom), `y` (left), `x2` (top), and `y2` (right) around it. -/// If `x2` or `y2` are unset, they mirror their opposing axis. -/// / scientific-auto: Draw set (used) axes `x` (bottom), `y` (left), `x2` (top) and `y2` (right) around -/// the plotting area, forming a rect if all axes are in use or a L-shape if only `x` and `y` are in use. -/// / school-book: Draw axes `x` (horizontal) and `y` (vertical) as arrows pointing to the right/top with both crossing at $(0, 0)$ -/// / left: Draw axes `x` and `y` as arrows, while the y axis stays on the left (at `x.min`) -/// and the x axis at the bottom (at `y.min`) -/// / `none`: Draw no axes (and no ticks). -/// -/// #example(``` -/// let opts = (x-tick-step: none, y-tick-step: none, size: (2,1)) -/// let data = cetz.plot.add(((-1,-1), (1,1),), mark: "o") -/// -/// for name in (none, "school-book", "left", "scientific") { -/// plot.plot(axis-style: name, ..opts, data, name: "plot") -/// content(((0,-1), "-|", "plot.south"), repr(name)) -/// set-origin((3.5,0)) -/// } -/// ```, vertical: true) -/// - plot-style (style,function): Styling to use for drawing plot graphs. -/// This style gets inherited by all plots and supports `palette` functions. -/// The following style keys are supported: -/// #show-parameter-block("stroke", ("none", "stroke"), default: 1pt, [ -/// Stroke style to use for stroking the graph. -/// ]) -/// #show-parameter-block("fill", ("none", "paint"), default: none, [ -/// Paint to use for filled graphs. Note that not all graphs may support filling and -/// that you may have to enable filling per graph, see `plot.add(fill: ..)`. -/// ]) -/// - mark-style (style,function): Styling to use for drawing plot marks. -/// This style gets inherited by all plots and supports `palette` functions. -/// The following style keys are supported: -/// #show-parameter-block("stroke", ("none", "stroke"), default: 1pt, [ -/// Stroke style to use for stroking the mark. -/// ]) -/// #show-parameter-block("fill", ("none", "paint"), default: none, [ -/// Paint to use for filling marks. -/// ]) -/// - fill-below (bool): If true, the filled shape of plots is drawn _below_ axes. -/// - name (string): The plots element name to be used when referring to anchors -/// - legend (none, auto, coordinate): The position the legend will be drawn at. See plot-legends for information about legends. If set to ``, the legend's "default-placement" styling will be used. If set to a ``, it will be taken as relative to the plot's origin. -/// - legend-anchor (auto, string): Anchor of the legend group to use as its origin. -/// If set to `auto` and `lengend` is one of the predefined legend anchors, the -/// opposite anchor to `legend` gets used. -/// - 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, - plot-style: default-plot-style, - mark-style: default-mark-style, - fill-below: true, - legend: auto, - legend-anchor: auto, - legend-style: (:), - ..options - ) = draw.group(name: name, ctx => { - // TODO: Assert cetz min version here! +// Get the default axis orientation +// depending on the axis name +#let get-default-axis-horizontal(name) = { + return lower(name).starts-with("x") +} + +// Consider splitting into sevaral files +#let _handle-named-axis-args(ctx, axis-dict, options, plot-size) = { + + // Get axis option for name + let get-axis-option(axis-name, name, default) = { + let v = options.at(axis-name + "-" + name, default: default) + if v == auto { default } else { v } + } - // Create plot context object - let make-ctx(x, y, size) = { - assert(x != none, message: "X axis does not exist") - assert(y != none, message: "Y axis does not exist") - assert(size.at(0) > 0 and size.at(1) > 0, message: "Plot size must be > 0") + for (name, axis) in axis-dict { + if not "ticks" in axis { axis.ticks = () } + axis.label = get-axis-option(name, "label", $#name$) - let x-scale = ((x.max - x.min) / size.at(0)) - let y-scale = ((y.max - y.min) / size.at(1)) + // Configure axis bounds + axis.min = get-axis-option(name, "min", axis.min) + axis.max = get-axis-option(name, "max", axis.max) - if y.horizontal { - (x-scale, y-scale) = (y-scale, x-scale) + assert(axis.min not in (none, auto) and + axis.max not in (none, auto), + message: "Axis min and max must be set.") + if axis.min == axis.max { + axis.min -= 1; axis.max += 1 } - return (x: x, y: y, size: size, x-scale: x-scale, y-scale: y-scale) + 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 } - // Setup data viewport - let data-viewport(data, x, y, size, body, name: none) = { - if body == none or body == () { return } - - assert.ne(x.horizontal, y.horizontal, - message: "Data must use one horizontal and one vertical axis!") - - // If y is the horizontal axis, swap x and y - // coordinates by swapping the transformation - // matrix columns. - if y.horizontal { - (x, y) = (y, x) - body = draw.set-ctx(ctx => { - ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) - return ctx - }) + body + // 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(other.horizontal != axis.horizontal, + message: "Equal axes must have opposing orientation.") + + let (w, h) = plot-size + let ratio = if other.horizontal { + h / w + } else { + w / h + } + axis.min = other.min * ratio + axis.max = other.max * ratio + + changed = true } - // Setup the viewport - axes.axis-viewport(size, x, y, none, body, name: name) + if changed { + axis-dict.at(name) = axis + } } - let data = () - let anchors = () - let annotations = () - let body = if body != none { body } else { () } - - 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 { data.push(cmd) } + for (name, axis) in axis-dict { + axis-dict.at(name) = axes.prepare-axis(ctx, axis, name) } - assert(axis-style in (none, "scientific", "scientific-auto", "school-book", "left"), - message: "Invalid plot style") + return axis-dict +} - // Create axes for data & annotations +#let _create-axis-dict(ctx, data, anchors, annotations, options, size) = { let axis-dict = (:) for d in data + annotations { if "axes" not in d { continue } @@ -309,9 +170,35 @@ } // Set axis options - axis-dict = plot-util.setup-axes(ctx, axis-dict, options.named(), size) + axis-dict = _handle-named-axis-args(ctx, axis-dict, options.named(), size) + return axis-dict +} + +#let _destructure-body(body) = { - // Prepare styles + // early exit + if body == none {return ((),(),())} + + let data = () + let anchors = () + let annotations = () + 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 { + data.push(cmd) + } + } + + return (data, anchors, annotations) +} + +#let _prepare-data-styles(data, plot-style, mark-style) = { for i in range(data.len()) { if "style" not in data.at(i) { continue } @@ -351,6 +238,84 @@ } } } + return data +} + +/// 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:`. +/// - body (body): Calls of `plot.add` or `plot.add-*` commands. Note that normal drawing +/// commands like `line` or `rect` are not allowed inside the plots body, instead wrap +/// them in `plot.annotate`, which lets you select the axes used for drawing. +/// +/// ```example +/// cetz-plot.plot({ +/// cetz-plot.add.xy(calc.sin, domain: (0,2*calc.pi)) +/// }) +/// ``` +/// - size (array): Plot size tuple of `(, )` in canvas units. +/// This is the plots inner plotting size without axes and labels. +/// this value, as it doesn't include axis labels, ticks, or the legend. +/// ```example +/// cetz-plot.plot( +/// size: (5,1), +/// x-tick-step: none, y-tick-step: none, +/// {cetz-plot.add.xy(calc.sin, domain: (0,2*calc.pi))} +/// ) +/// ``` +/// - axis-style (axis-style-module): TODO: Make this link to the axis-style section +/// ```example +/// cetz-plot.plot( +/// size: (5,5), +/// axis-style: cetz-plot.axis-style.polar-2d, +/// x-grid: "both", y-grid: "both", +/// {cetz-plot.add.xy(calc.sin, domain: (0,2*calc.pi))} +/// ) +/// ``` +/// - name (string, none): The plots element name to be used when referring to anchors +/// - plot-style (style,function): Styling to use for drawing plot graphs. +/// This style gets inherited by all plots and supports `palette` functions. +/// The following style keys are supported: +/// #show-parameter-block("stroke", ("none", "stroke"), default: 1pt, [ +/// Stroke style to use for stroking the graph. +/// ]) +/// #show-parameter-block("fill", ("none", "paint"), default: none, [ +/// Paint to use for filled graphs. Note that not all graphs may support filling and +/// that you may have to enable filling per graph, see `plot.add(fill: ..)`. +/// ]) +/// - mark-style (style,function): Styling to use for drawing plot marks. +/// This style gets inherited by all plots and supports `palette` functions. +/// The following style keys are supported: +/// #show-parameter-block("stroke", ("none", "stroke"), default: 1pt, [ +/// Stroke style to use for stroking the mark. +/// ]) +/// #show-parameter-block("fill", ("none", "paint"), default: none, [ +/// Paint to use for filling marks. +/// ]) +/// - legend (none, auto, coordinate): The position the legend will be drawn at. See plot-legends for information about legends. If set to ``, the legend's "default-placement" styling will be used. If set to a ``, it will be taken as relative to the plot's origin. +/// - legend-anchor (auto, string): Anchor of the legend group to use as its origin. +/// If set to `auto` and `lengend` is one of the predefined legend anchors, the +/// opposite anchor to `legend` gets used. +/// - 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: (5,5 * 3/4), + axis-style: axis-style.orthorect-2d, + name: none, + plot-style: default-plot-style, + mark-style: default-mark-style, + legend: auto, + legend-anchor: auto, + legend-style: (:), + ..options +) = draw.group(name: name, ctx => { + + // TODO: Assert cetz min version here! + + let (data, anchors, annotations) = _destructure-body(body) + let axis-dict = _create-axis-dict(ctx, data, anchors, annotations, options, size) + data = _prepare-data-styles(data, plot-style, mark-style) draw.group(name: "plot", { draw.anchor("origin", (0, 0)) @@ -359,8 +324,8 @@ for i in range(data.len()) { if "axes" not in data.at(i) { continue } - let (x, y) = data.at(i).axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let axes = data.at(i).axes.map(name => axis-dict.at(name)) + let plot-ctx = axis-style.make-ctx(axes, size) if "plot-prepare" in data.at(i) { data.at(i) = (data.at(i).plot-prepare)(data.at(i), plot-ctx) @@ -371,82 +336,45 @@ // Background Annotations for a in annotations.filter(a => a.background) { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let axes = a.axes.map(name => axis-dict.at(name)) + let plot-ctx = axis-style.make-ctx(axes, size) - data-viewport(a, x, y, size, { + axis-style.data-viewport(axes, size, { draw.anchor("default", (0, 0)) a.body }) } // Fill - if fill-below { - for d in data { - if "axes" not in d { continue } + for d in data { + if "axes" not in d { continue } - let (x, y) = d.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let axes = d.axes.map(name => axis-dict.at(name)) + let plot-ctx = axis-style.make-ctx(axes, size) - data-viewport(d, x, y, size, { - draw.anchor("default", (0, 0)) - draw.set-style(..d.style) + axis-style.data-viewport(axes, size, { + draw.anchor("default", (0, 0)) + draw.set-style(..d.style) - if "plot-fill" in d { - (d.plot-fill)(d, plot-ctx) - } - }) - } + if "plot-fill" in d { + (d.plot-fill)(d, plot-ctx) + } + }) } - 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: axis-dict.at("x", default: none), - top: axis-dict.at("x2", default: mirror), - left: axis-dict.at("y", default: none), - right: axis-dict.at("y2", default: mirror),) - } else if axis-style == "left" { - axes.school-book( - size: size, - axis-dict.x, - axis-dict.y, - x-position: axis-dict.y.min, - y-position: axis-dict.x.min) - } else if axis-style == "school-book" { - axes.school-book( - size: size, - axis-dict.x, - axis-dict.y,) - } + axis-style.draw-axes(size, axis-dict) // Stroke + Mark data for d in data { if "axes" not in d { continue } - let (x, y) = d.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let axes = d.axes.map(name => axis-dict.at(name)) + let plot-ctx = axis-style.make-ctx(axes, size) - data-viewport(d, x, y, size, { + axis-style.data-viewport(axes, size, { 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) - } if "plot-stroke" in d { (d.plot-stroke)(d, plot-ctx) } @@ -455,17 +383,17 @@ if "mark" in d and d.mark != none { draw.group({ draw.set-style(..d.style, ..d.mark-style) - mark.draw-mark(d.data, x, y, d.mark, d.mark-size, size) + mark.draw-mark(d.data, plot-ctx, d.mark, d.mark-size, size) }) } } // Foreground Annotations for a in annotations.filter(a => not a.background) { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let axes = a.axes.map(name => axis-dict.at(name)) + let plot-ctx = axis-style.make-ctx(axes, size) - data-viewport(a, x, y, size, { + axis-style.data-viewport(axes, size, { draw.anchor("default", (0, 0)) a.body }) @@ -473,8 +401,8 @@ // Place anchors for a in anchors { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let axes = a.axes.map(name => axis-dict.at(name)) + let plot-ctx = make-ctx(axes, size) let pt = a.position.enumerate().map(((i, v)) => { if v == "min" { return axis-dict.at(a.axes.at(i)).min } @@ -488,14 +416,20 @@ } }) - // 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(ctx.style, - base: plot-legend.default-style, merge: legend-style, root: "legend") + let legend-style = styles.resolve( + 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 { @@ -504,50 +438,18 @@ auto } - plot-legend.item(item.label, preview, + 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: (:))) + ..item.at("style", default: (:)) + ) } }, ..legend-style) + } } - draw.copy-anchors("plot") -}) - -/// Add an anchor to a plot environment -/// -/// This function is similar to `draw.anchor` but it takes an additional -/// axis tuple to specify which axis coordinate system to use. -/// -/// #example(``` -/// import cetz.plot -/// import cetz.draw: * -/// plot.plot(size: (2,2), name: "plot", -/// x-tick-step: none, y-tick-step: none, { -/// plot.add(((0,0), (1,1), (2,.5), (4,3))) -/// plot.add-anchor("pt", (1,1)) -/// }) -/// -/// line("plot.pt", ((), "|-", (0,1.5)), mark: (start: ">"), name: "line") -/// content("line.end", [Here], anchor: "south", padding: .1) -/// ```) -/// -/// - name (string): Anchor name -/// - position (tuple): Tuple of x and y values. -/// Both values can have the special values "min" and -/// "max", which resolve to the axis min/max value. -/// Position is in axis space defined by the axes passed to `axes`. -/// - axes (tuple): Name of the axes to use `("x", "y")` as coordinate -/// system for `position`. Note that both axes must be used, -/// 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, - ),) -} +}) \ No newline at end of file diff --git a/src/plot/add.typ b/src/plot/add.typ new file mode 100644 index 0000000..abc80a3 --- /dev/null +++ b/src/plot/add.typ @@ -0,0 +1,10 @@ +#import "elements/anchor.typ": anchor +#import "elements/annotation.typ": annotation +#import "elements/series.typ": series + +// Continuous Continuous +#import "elements/xy.typ": xy, hline, vline, fill-between +#import "elements/errorbar.typ": errorbar + +// Categorical Continuous +#import "elements/violin.typ": violin diff --git a/src/plot/axis-style.typ b/src/plot/axis-style.typ new file mode 100644 index 0000000..81b09cd --- /dev/null +++ b/src/plot/axis-style.typ @@ -0,0 +1,5 @@ +// #import "axis-styles/barycentric-2d/barycentric-2d.typ" +#import "axis-styles/polar-2d/impl.typ" as polar-2d +#import "axis-styles/orthorect-2d/impl.typ" as orthorect-2d +// #import "axis-styles/barycentric-2d/barycentric-2d.typ" +// #import "axis-styles/barycentric-2d/barycentric-2d.typ" \ No newline at end of file diff --git a/src/plot/axis-styles/barycentric-2d/barycentric-2d.typ b/src/plot/axis-styles/barycentric-2d/barycentric-2d.typ new file mode 100644 index 0000000..e69de29 diff --git a/src/plot/axis-styles/generic.typ b/src/plot/axis-styles/generic.typ new file mode 100644 index 0000000..e69de29 diff --git a/src/plot/axis-styles/orthorect-2d/axis.typ b/src/plot/axis-styles/orthorect-2d/axis.typ new file mode 100644 index 0000000..8597321 --- /dev/null +++ b/src/plot/axis-styles/orthorect-2d/axis.typ @@ -0,0 +1,110 @@ +#import "/src/cetz.typ": draw, util, vector + +#let inset-axis-points(ctx, style, axis, start, end) = { + if axis == none { return (start, end) } + + let (low, high) = axis.inset.map(v => util.resolve-number(ctx, v)) + + let is-horizontal = start.at(1) == end.at(1) + if is-horizontal { + start = vector.add(start, (low, 0)) + end = vector.sub(end, (high, 0)) + } else { + start = vector.add(start, (0, low)) + end = vector.sub(end, (0, high)) + } + return (start, end) +} + +#let draw-axis-line(start, end, axis, is-horizontal, style) = { + let enabled = if axis != none and axis.show-break { + axis.min > 0 or axis.max < 0 + } else { false } + + if enabled { + let size = if is-horizontal { + (style.break-point.width, 0) + } else { + (0, style.break-point.width, 0) + } + + let up = if is-horizontal { + (0, style.break-point.length) + } else { + (style.break-point.length, 0) + } + + let add-break(is-end) = { + let a = () + let b = (rel: vector.scale(size, .3), update: false) + let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false) + let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false) + let e = (rel: vector.scale(size, .7), update: false) + let f = (rel: size) + + let mark = if is-end { + style.at("mark", default: none) + } + draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark) + } + + draw.merge-path({ + draw.move-to(start) + if axis.min > 0 { + add-break(false) + draw.line((rel: size, to: start), end, mark: style.at("mark", default: none)) + } else if axis.max < 0 { + draw.line(start, (rel: vector.scale(size, -1), to: end)) + add-break(true) + } + }, stroke: style.stroke) + } else { + draw.line(start, end, stroke: style.stroke, mark: style.at("mark", default: none)) + } +} + +// 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) + } + } +} \ No newline at end of file diff --git a/src/plot/axis-styles/orthorect-2d/clipper.typ b/src/plot/axis-styles/orthorect-2d/clipper.typ new file mode 100644 index 0000000..8dd9e1a --- /dev/null +++ b/src/plot/axis-styles/orthorect-2d/clipper.typ @@ -0,0 +1,184 @@ +/// Clip line-strip in rect +/// +/// - points (array): Array of vectors representing a line-strip +/// - low (vector): Lower clip-window coordinate +/// - high (vector): Upper clip-window coordinate +/// -> array List of line-strips representing the paths insides the clip-window +#let clipped-paths-rect(points, ctx, fill: false) = { + let (low, high) = ctx.clip + let (min-x, max-x) = (calc.min(low.at(0), high.at(0)), + calc.max(low.at(0), high.at(0))) + let (min-y, max-y) = (calc.min(low.at(1), high.at(1)), + calc.max(low.at(1), high.at(1))) + + let in-rect(pt) = { + return (pt.at(0) >= min-x and pt.at(0) <= max-x and + pt.at(1) >= min-y and pt.at(1) <= max-y) + } + + let interpolated-end(a, b) = { + if in-rect(a) and in-rect(b) { + return b + } + + let (x1, y1, ..) = a + let (x2, y2, ..) = b + + if x2 - x1 == 0 { + return (x2, calc.min(max-y, calc.max(y2, min-y))) + } + + if y2 - y1 == 0 { + return (calc.min(max-x, calc.max(x2, min-x)), y2) + } + + let m = (y2 - y1) / (x2 - x1) + let n = y2 - m * x2 + + let x = x2 + let y = y2 + + y = calc.min(max-y, calc.max(y, min-y)) + x = (y - n) / m + + x = calc.min(max-x, calc.max(x, min-x)) + y = m * x + n + + return (x, y) + } + + // Append path to paths and return paths + // + // If path starts or ends with a vector of another part, merge those + // paths instead appending path as a new path. + let append-path(paths, path) = { + if path.len() <= 1 { + return paths + } + + let cmp(a, b) = { + return a.map(calc.round.with(digits: 8)) == b.map(calc.round.with(digits: 8)) + } + + let added = false + for i in range(0, paths.len()) { + let p = paths.at(i) + if cmp(p.first(), path.last()) { + paths.at(i) = path + p + added = true + } else if cmp(p.first(), path.first()) { + paths.at(i) = path.rev() + p + added = true + } else if cmp(p.last(), path.first()) { + paths.at(i) = p + path + added = true + } else if cmp(p.last(), path.last()) { + paths.at(i) = p + path.rev() + added = true + } + if added { break } + } + + if not added { + paths.push(path) + } + return paths + } + + let clamped-pt(pt) = { + return (calc.max(min-x, calc.min(pt.at(0), max-x)), + calc.max(min-y, calc.min(pt.at(1), max-y))) + } + + let paths = () + + let path = () + let prev = points.at(0) + let was-inside = in-rect(prev) + if was-inside { + path.push(prev) + } else if fill { + path.push(clamped-pt(prev)) + } + + for i in range(1, points.len()) { + let prev = points.at(i - 1) + let pt = points.at(i) + + let is-inside = in-rect(pt) + + let (x1, y1) = prev + let (x2, y2) = pt + + // Ignore lines if both ends are outsides the x-window and on the + // same side. + if (x1 < min-x and x2 < min-x) or (x1 > max-x and x2 > max-x) { + if fill { + let clamped = clamped-pt(pt) + if path.last() != clamped { + path.push(clamped) + } + } + was-inside = false + continue + } + + if is-inside { + if was-inside { + path.push(pt) + } else { + path.push(interpolated-end(pt, prev)) + path.push(pt) + } + } else { + if was-inside { + path.push(interpolated-end(prev, pt)) + } else { + let (a, b) = (interpolated-end(pt, prev), + interpolated-end(prev, pt)) + if in-rect(a) and in-rect(b) { + path.push(a) + path.push(b) + } else if fill { + let clamped = clamped-pt(pt) + if path.last() != clamped { + path.push(clamped) + } + } + } + + if path.len() > 0 and not fill { + paths = append-path(paths, path) + path = () + } + } + + was-inside = is-inside + } + + // Append clamped last point if filling + if fill and not in-rect(points.last()) { + path.push(clamped-pt(points.last())) + } + + if path.len() > 1 { + paths = append-path(paths, path) + } + + return paths +} + +/// Compute clipped stroke paths +/// +/// - points (array): X/Y data points +/// - low (vector): Lower clip-window coordinate +/// - high (vector): Upper clip-window coordinate +/// -> array List of stroke paths +#let compute-stroke-paths = clipped-paths-rect.with(fill: false) +/// Compute clipped fill path +/// +/// - points (array): X/Y data points +/// - low (vector): Lower clip-window coordinate +/// - high (vector): Upper clip-window coordinate +/// -> array List of fill paths +#let compute-fill-paths = clipped-paths-rect.with(fill: true) \ No newline at end of file diff --git a/src/plot/axis-styles/orthorect-2d/grid.typ b/src/plot/axis-styles/orthorect-2d/grid.typ new file mode 100644 index 0000000..33ada22 --- /dev/null +++ b/src/plot/axis-styles/orthorect-2d/grid.typ @@ -0,0 +1,46 @@ +#import "/src/cetz.typ": util, vector, draw + +// Refactor opporunity: +#let _get-grid-type(axis) = { + let grid = axis.ticks.at("grid", default: false) + if grid == "major" or grid == true { return 1 } + if grid == "minor" { return 2 } + if grid == "both" { return 3 } + return 0 +} + +// Draw grid lines for the ticks of an axis +// +// - cxt (context): +// - axis (dictionary): The axis +// - ticks (array): The computed ticks +// - low (vector): Start position of a grid-line at tick 0 +// - high (vector): End position of a grid-line at tick 0 +// - dir (vector): Normalized grid direction vector along the grid axis +// - style (style): Axis style +#let draw-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) + } + } + } +} \ No newline at end of file diff --git a/src/plot/axis-styles/orthorect-2d/impl.typ b/src/plot/axis-styles/orthorect-2d/impl.typ new file mode 100644 index 0000000..0c80f51 --- /dev/null +++ b/src/plot/axis-styles/orthorect-2d/impl.typ @@ -0,0 +1,2 @@ +#import "orthorect-2d.typ": make-ctx, data-viewport, draw-axes, +#import "transforms.typ": transform-vec \ No newline at end of file diff --git a/src/plot/axis-styles/orthorect-2d/orthorect-2d.typ b/src/plot/axis-styles/orthorect-2d/orthorect-2d.typ new file mode 100644 index 0000000..c2bdb05 --- /dev/null +++ b/src/plot/axis-styles/orthorect-2d/orthorect-2d.typ @@ -0,0 +1,173 @@ +#import "/src/cetz.typ": draw, util, styles, vector +#import "/src/plot/styles.typ": default-style, prepare-style, get-axis-style +#import "/src/axes/axes.typ" + +#import "grid.typ" +#import "axis.typ": draw-axis-line, inset-axis-points, place-ticks-on-line +#import "transforms.typ": data-viewport, axis-viewport, transform-vec +#import "clipper.typ" + +#let default-style-orthorect-2d = 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, + ) +) + + +#let make-ctx((x, y), size) = { + assert(x != none, message: "X axis does not exist") + assert(y != none, message: "Y axis does not exist") + assert(size.at(0) > 0 and size.at(1) > 0, message: "Plot size must be > 0") + + let x-scale = ((x.max - x.min) / size.at(0)) + let y-scale = ((y.max - y.min) / size.at(1)) + + if y.horizontal { + (x-scale, y-scale) = (y-scale, x-scale) + } + + return ( + axes: (x,y), + size: size, + x-scale: x-scale, + y-scale: y-scale, + clip: ((x.min, y.min), (x.max, y.max)), + transform-vec: transform-vec, + compute-stroke-paths: clipper.compute-stroke-paths, + compute-fill-paths: clipper.compute-fill-paths + ) +} + +#let draw-axes( + (w,h), + axis-dict, + name: none, + ..style +) = { + let bottom = axis-dict.at("x", default: none) + let top = axis-dict.at("x2", default: auto) + let left = axis-dict.at("y", default: none) + let right = axis-dict.at("y2", default: auto) + + if (top == auto){ + top = bottom + top.is-mirror = true + } + + if (right == auto){ + right = left + right.is-mirror = true + } + + draw.group(name: name, ctx => { + draw.anchor("origin", (0, 0)) + + // Handle style + let style = style.named() + style = styles.resolve( + ctx.style, + merge: style, + root: "axes", + base: default-style-orthorect-2d + ) + style = prepare-style(ctx, style) + + // Compute ticks + let x-ticks = axes.ticks.compute-ticks(bottom, style) + let y-ticks = axes.ticks.compute-ticks(left, style) + let x2-ticks = axes.ticks.compute-ticks(top, style) + let y2-ticks = axes.ticks.compute-ticks(right, style) + + // Draw frame + if style.fill != none { + draw.on-layer(style.background-layer, { + draw.rect((0,0), (w,h), fill: style.fill, stroke: none) + }) + } + + // Draw grid + draw.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 { + draw.on-layer(style.grid-layer, { + grid.draw-lines(ctx, axis, ticks, start, end, direction, style) + }) + } + } + }) + + // Draw axes + draw.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) + draw.on-layer(style.axis-layer, { + draw.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 + } + + draw.content((rel: offset, to: "axis." + group-anchor), + [#axis.label], + angle: angle, + anchor: content-anchor) + } + }) + } + }) + }) +} \ No newline at end of file diff --git a/src/plot/axis-styles/orthorect-2d/transforms.typ b/src/plot/axis-styles/orthorect-2d/transforms.typ new file mode 100644 index 0000000..15e191f --- /dev/null +++ b/src/plot/axis-styles/orthorect-2d/transforms.typ @@ -0,0 +1,87 @@ +#import "/src/cetz.typ": draw, matrix, process, util, drawable + +// Transform a single vector along a x, y and z axis +// +// - size (vector): Coordinate system size +// - x-axis (axis): X axis +// - y-axis (axis): Y axis +// - z-axis (axis): Z axis +// - vec (vector): Input vector to transform +// -> vector +#let transform-vec(size, axes, vec) = { + + let (x,y,) = for (dim, axis) in axes.enumerate() { + + let s = size.at(dim) - axis.inset.sum() + let o = 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 range = transform-func(axis.max) - transform-func(axis.min) + + let f = s / range + ((transform-func(vec.at(dim)) - transform-func(axis.min)) * f + o,) + } + + return (x, y, 0) +} + +// Draw inside viewport coordinates of two axes +// +// - size (vector): Axis canvas size (relative to origin) +// - x (axis): Horizontal axis +// - y (axis): Vertical axis +// - z (axis): Z axis +// - name (string,none): Group name +#let axis-viewport(size,(x, y,), 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(size, (x, y), pt) + })) + }) + } + if "pos" in d { + d.pos = transform-vec(size, (x, y), d.pos) + } + return d + }) + + return ( + ctx: ctx, + drawables: drawable.apply-transform(ctx.transform, drawables) + ) + },)) +} + +#let data-viewport((x, y), size, body, name: none) = { + if body == none or body == () { return } + + assert.ne(x.horizontal, y.horizontal, + message: "Data must use one horizontal and one vertical axis!") + + // If y is the horizontal axis, swap x and y + // coordinates by swapping the transformation + // matrix columns. + if y.horizontal { + (x, y) = (y, x) + body = draw.set-ctx(ctx => { + ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) + return ctx + }) + body + } + + // Setup the viewport + axis-viewport(size, (x,y), body, name: name) +} \ No newline at end of file diff --git a/src/plot/axis-styles/polar-2d/axis.typ b/src/plot/axis-styles/polar-2d/axis.typ new file mode 100644 index 0000000..54e2fb3 --- /dev/null +++ b/src/plot/axis-styles/polar-2d/axis.typ @@ -0,0 +1,164 @@ +#import "/src/cetz.typ": draw, util, vector + +#let inset-axis-points(ctx, style, axis, start, end) = { + if axis == none { return (start, end) } + + let (low, high) = axis.inset.map(v => util.resolve-number(ctx, v)) + + let is-horizontal = start.at(1) == end.at(1) + if is-horizontal { + start = vector.add(start, (low, 0)) + end = vector.sub(end, (high, 0)) + } else { + start = vector.add(start, (0, low)) + end = vector.sub(end, (0, high)) + } + return (start, end) +} + +#let draw-axis-line(start, end, axis, is-horizontal, style) = { + let enabled = if axis != none and axis.show-break { + axis.min > 0 or axis.max < 0 + } else { false } + + if enabled { + let size = if is-horizontal { + (style.break-point.width, 0) + } else { + (0, style.break-point.width, 0) + } + + let up = if is-horizontal { + (0, style.break-point.length) + } else { + (style.break-point.length, 0) + } + + let add-break(is-end) = { + let a = () + let b = (rel: vector.scale(size, .3), update: false) + let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false) + let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false) + let e = (rel: vector.scale(size, .7), update: false) + let f = (rel: size) + + let mark = if is-end { + style.at("mark", default: none) + } + draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark) + } + + draw.merge-path({ + draw.move-to(start) + if axis.min > 0 { + add-break(false) + draw.line((rel: size, to: start), end, mark: style.at("mark", default: none)) + } else if axis.max < 0 { + draw.line(start, (rel: vector.scale(size, -1), to: end)) + add-break(true) + } + }, stroke: style.stroke) + } else { + draw.line(start, end, stroke: style.stroke, mark: style.at("mark", default: none)) + } +} + +// 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) + } + } +} + +#let place-ticks-on-radius(ticks, radius, style) = { + + let center = (radius,radius) + + // Early exit + let show-label = style.tick.label.show + if (show-label not in (auto, true)) {return} + + let def(v, d) = { + return if v == none or v == auto {d} else {v} + } + + for (distance, label, is-major) in ticks { + + // Early exit for overlapping tick + if (distance == 1){continue} + + let theta = (2 * distance) * calc.pi + let dist = radius + + let offset = style.tick.offset + let length = if is-major { style.tick.length } else { style.tick.minor-length } + + let a = dist + offset + let b = a - length + + draw.line( + (a * calc.sin(theta) + radius, a * calc.cos(theta) + radius), + (b * calc.sin(theta) + radius, b * calc.cos(theta) + radius), + stroke: style.tick.stroke + ) + + if (label != none){ + let offset = style.tick.label.offset + + // let c = vector.sub(if length <= 0 { b } else { a }, + // vector.scale(norm, offset)) + + let c = a + offset + + let angle = def(style.tick.label.angle, 0deg) + let anchor = def(style.tick.label.anchor, "center") + + draw.content( + (c * calc.sin(theta) + radius, c * calc.cos(theta) + radius), + [#label], + angle: angle, + anchor: anchor + ) + } + } + +} \ No newline at end of file diff --git a/src/plot/axis-styles/polar-2d/clipper.typ b/src/plot/axis-styles/polar-2d/clipper.typ new file mode 100644 index 0000000..1241da6 --- /dev/null +++ b/src/plot/axis-styles/polar-2d/clipper.typ @@ -0,0 +1,187 @@ +#import "/src/cetz.typ" +#import cetz.util: bezier + +/// Clip line-strip in rect +/// +/// - points (array): Array of vectors representing a line-strip +/// - low (vector): Lower clip-window coordinate +/// - high (vector): Upper clip-window coordinate +/// -> array List of line-strips representing the paths insides the clip-window +#let clipped-paths-circle(points, ctx, fill: false) = { + let (low, high) = ctx.clip + let (min-x, max-x) = (calc.min(low.at(0), high.at(0)), + calc.max(low.at(0), high.at(0))) + let (min-y, max-y) = (calc.min(low.at(1), high.at(1)), + calc.max(low.at(1), high.at(1))) + + let in-rect(pt) = { + return (pt.at(0) >= min-x and pt.at(0) <= max-x and + pt.at(1) >= min-y and pt.at(1) <= max-y) + } + + let interpolated-end(a, b) = { + if in-rect(a) and in-rect(b) { + return b + } + + let (x1, y1, ..) = a + let (x2, y2, ..) = b + + if x2 - x1 == 0 { + return (x2, calc.min(max-y, calc.max(y2, min-y))) + } + + if y2 - y1 == 0 { + return (calc.min(max-x, calc.max(x2, min-x)), y2) + } + + let m = (y2 - y1) / (x2 - x1) + let n = y2 - m * x2 + + let x = x2 + let y = y2 + + y = calc.min(max-y, calc.max(y, min-y)) + x = (y - n) / m + + x = calc.min(max-x, calc.max(x, min-x)) + y = m * x + n + + return (x, y) + } + + // Append path to paths and return paths + // + // If path starts or ends with a vector of another part, merge those + // paths instead appending path as a new path. + let append-path(paths, path) = { + if path.len() <= 1 { + return paths + } + + let cmp(a, b) = { + return a.map(calc.round.with(digits: 8)) == b.map(calc.round.with(digits: 8)) + } + + let added = false + for i in range(0, paths.len()) { + let p = paths.at(i) + if cmp(p.first(), path.last()) { + paths.at(i) = path + p + added = true + } else if cmp(p.first(), path.first()) { + paths.at(i) = path.rev() + p + added = true + } else if cmp(p.last(), path.first()) { + paths.at(i) = p + path + added = true + } else if cmp(p.last(), path.last()) { + paths.at(i) = p + path.rev() + added = true + } + if added { break } + } + + if not added { + paths.push(path) + } + return paths + } + + let clamped-pt(pt) = { + return (calc.max(min-x, calc.min(pt.at(0), max-x)), + calc.max(min-y, calc.min(pt.at(1), max-y))) + } + + let paths = () + + let path = () + let prev = points.at(0) + let was-inside = in-rect(prev) + if was-inside { + path.push(prev) + } else if fill { + path.push(clamped-pt(prev)) + } + + for i in range(1, points.len()) { + let prev = points.at(i - 1) + let pt = points.at(i) + + let is-inside = in-rect(pt) + + let (x1, y1) = prev + let (x2, y2) = pt + + // Ignore lines if both ends are outsides the x-window and on the + // same side. + if (x1 < min-x and x2 < min-x) or (x1 > max-x and x2 > max-x) { + if fill { + let clamped = clamped-pt(pt) + if path.last() != clamped { + path.push(clamped) + } + } + was-inside = false + continue + } + + if is-inside { + if was-inside { + path.push(pt) + } else { + path.push(interpolated-end(pt, prev)) + path.push(pt) + } + } else { + if was-inside { + path.push(interpolated-end(prev, pt)) + } else { + let (a, b) = (interpolated-end(pt, prev), + interpolated-end(prev, pt)) + if in-rect(a) and in-rect(b) { + path.push(a) + path.push(b) + } else if fill { + let clamped = clamped-pt(pt) + if path.last() != clamped { + path.push(clamped) + } + } + } + + if path.len() > 0 and not fill { + paths = append-path(paths, path) + path = () + } + } + + was-inside = is-inside + } + + // Append clamped last point if filling + if fill and not in-rect(points.last()) { + path.push(clamped-pt(points.last())) + } + + if path.len() > 1 { + paths = append-path(paths, path) + } + + return paths +} + +/// Compute clipped stroke paths +/// +/// - points (array): X/Y data points +/// - low (vector): Lower clip-window coordinate +/// - high (vector): Upper clip-window coordinate +/// -> array List of stroke paths +#let compute-stroke-paths = clipped-paths-circle.with(fill: false) +/// Compute clipped fill path +/// +/// - points (array): X/Y data points +/// - low (vector): Lower clip-window coordinate +/// - high (vector): Upper clip-window coordinate +/// -> array List of fill paths +#let compute-fill-paths = clipped-paths-circle.with(fill: true) \ No newline at end of file diff --git a/src/plot/axis-styles/polar-2d/grid.typ b/src/plot/axis-styles/polar-2d/grid.typ new file mode 100644 index 0000000..596dab3 --- /dev/null +++ b/src/plot/axis-styles/polar-2d/grid.typ @@ -0,0 +1,59 @@ +#import "/src/cetz.typ": util, vector, draw + +// Refactor opporunity: +#let _get-grid-type(axis) = { + let grid = axis.ticks.at("grid", default: false) + if grid == "major" or grid == true { return 1 } + if grid == "minor" { return 2 } + if grid == "both" { return 3 } + return 0 +} + +// Draw grid lines for the ticks of an axis +// +// - cxt (context): +// - axis (dictionary): The axis +// - ticks (array): The computed ticks +// - low (vector): Start position of a grid-line at tick 0 +// - high (vector): End position of a grid-line at tick 0 +// - dir (vector): Normalized grid direction vector along the grid axis +// - style (style): Axis style +#let draw-lines(ctx, axis, ticks, radius, style) = { + let offset = (0,0) + if axis.inset != none { + let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v)) + offset = inset-low + } + let kind = _get-grid-type(axis) + if kind == 0 {return} + + if axis.horizontal { + for (distance, label, is-major) in ticks { + let theta = distance * calc.pi * 2 + draw.line( + (radius, radius), + ( + radius * (calc.cos(theta) + 1), + radius * (calc.sin(theta) + 1) + ), + stroke: if is-major and (kind == 1 or kind == 3) { + style.grid.stroke + } else if not is-major and kind >= 2 { + style.minor-grid.stroke + } + ) + } + } else { + for (distance, label, is-major) in ticks { + draw.circle( + (radius, radius), + radius: distance * radius, + stroke: if is-major and (kind == 1 or kind == 3) { + style.grid.stroke + } else if not is-major and (kind >= 2) { + style.minor-grid.stroke + } + ) + } + } +} \ No newline at end of file diff --git a/src/plot/axis-styles/polar-2d/impl.typ b/src/plot/axis-styles/polar-2d/impl.typ new file mode 100644 index 0000000..7d4af6d --- /dev/null +++ b/src/plot/axis-styles/polar-2d/impl.typ @@ -0,0 +1,2 @@ +#import "polar-2d.typ": make-ctx, data-viewport, draw-axes +#import "transforms.typ": transform-vec \ No newline at end of file diff --git a/src/plot/axis-styles/polar-2d/polar-2d.typ b/src/plot/axis-styles/polar-2d/polar-2d.typ new file mode 100644 index 0000000..44727a7 --- /dev/null +++ b/src/plot/axis-styles/polar-2d/polar-2d.typ @@ -0,0 +1,189 @@ +#import "/src/cetz.typ": draw, util, styles, vector +#import "/src/plot/styles.typ": default-style, prepare-style, get-axis-style +#import "/src/axes/axes.typ" + +#import "grid.typ" +#import "axis.typ": draw-axis-line, inset-axis-points, place-ticks-on-line, place-ticks-on-radius +#import "transforms.typ": data-viewport, axis-viewport, transform-vec +#import "clipper.typ" + +#let default-style-polar-2d = util.merge-dictionary( + default-style, + ( + distal: (tick: (label: (anchor: "north-east", offset: -0.2))), + angular: (tick: (label: (anchor: "center", offset: 0.35,), length: 5pt)), + stroke: (cap: "square"), + padding: 0, + ) +) + +// Consider refactor +#let make-ctx((x, y), size) = { + assert(x != none, message: "X axis does not exist") + assert(y != none, message: "Y axis does not exist") + assert(size.at(0) > 0 and size.at(1) > 0, message: "Plot size must be > 0") + + let x-scale = ((x.max - x.min) / size.at(0)) + let y-scale = ((y.max - y.min) / size.at(1)) + + if y.horizontal { + (x-scale, y-scale) = (y-scale, x-scale) + } + + return ( + axes: (x,y), + size: size, + x-scale: x-scale, + y-scale: y-scale, + clip: ((x.min, y.min), (x.max, y.max)), // TODO: Change to radius + transform-vec: transform-vec, + compute-stroke-paths: clipper.compute-stroke-paths, + compute-fill-paths: clipper.compute-fill-paths + ) +} + +#let draw-axes( + (w,h), + axis-dict, + name: none, + ..style +) = { + let angular = axis-dict.at("x", default: none) + let distal = axis-dict.at("y", default: none) + + let radius = calc.min(w,h)/2 + + draw.group(name: name, ctx => { + draw.anchor("origin", (radius, radius)) + + // Handle style + let style = style.named() + style = styles.resolve( + ctx.style, + merge: style, + root: "axes", + base: default-style-polar-2d + ) + style = prepare-style(ctx, style) + + // Compute ticks + let angular-ticks = axes.ticks.compute-ticks(angular, style) + let distal-ticks = axes.ticks.compute-ticks(distal, style) + + // Draw frame + if style.fill != none { + draw.on-layer(style.background-layer, { + draw.circle("origin", radius: radius, fill: style.fill, stroke: none) + }) + } + + // Draw grid + draw.group(name: "grid", ctx => { + let axes = ( + ("x", angular-ticks, angular), + ("y", distal-ticks, distal) + ) + for (name, ticks, axis) in axes { + if axis == none { continue } + + let style = get-axis-style(ctx, style, name) + draw.on-layer(style.grid-layer, { + grid.draw-lines(ctx, axis, ticks, radius, style) + }) + } + }) + + // Draw axes + draw.group(name: "axes", { + + // Render distal + draw.on-layer(style.axis-layer, { + draw.group(name: "axis", { + if distal != none { + // To do: Allow finer control over placement + draw.line( + "origin", (rel:(0, radius)), + stroke: style.stroke, + mark: style.at("mark", default: none) + ) + + place-ticks-on-line( + distal-ticks, + (radius, radius), + (radius, radius*2), + prepare-style(ctx, style.distal), + ) + } + }) + }) + + draw.on-layer(style.axis-layer, { + draw.group(name: "axis", { + if angular != none { + // To do: Allow finer control over placement + draw.circle( + "origin", + radius: radius, + stroke: style.stroke, + mark: style.at("mark", default: none) + ) + + place-ticks-on-radius( + angular-ticks, + (radius), + prepare-style(ctx, style.angular), + ) + } + }) + }) + // let axes = ( + // ("angular", (0, 0), (w, 0), (0, -1), false, angular-ticks, angular,), + // ("distal", (0, 0), (0, h), (-1, 0), true, distal-ticks, distal,), + // ) + // let label-placement = ( + // angular: ("south", "north", 0deg), + // distal: ("north", "south", 0deg), + // ) + + // 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) + + // draw.on-layer(style.axis-layer, { + // draw.group(name: "axis", { + // if axis != none { + // draw-axis-line(start, end, axis, is-horizontal, style) + // 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 + // } + + // draw.content((rel: offset, to: "axis." + group-anchor), + // [#axis.label], + // angle: angle, + // anchor: content-anchor) + // } + // }) + // } + }) + }) +} \ No newline at end of file diff --git a/src/plot/axis-styles/polar-2d/transforms.typ b/src/plot/axis-styles/polar-2d/transforms.typ new file mode 100644 index 0000000..f2bac4d --- /dev/null +++ b/src/plot/axis-styles/polar-2d/transforms.typ @@ -0,0 +1,80 @@ +#import "/src/cetz.typ": draw, matrix, process, util, drawable + +// Transform a single vector along a x, y and z axis +// +// - size (vector): Coordinate system size +// - x-axis (axis): X axis +// - y-axis (axis): Y axis +// - z-axis (axis): Z axis +// - vec (vector): Input vector to transform +// -> vector +#let transform-vec(size, (angular, distal), vec) = { + + let radius = calc.min(..size) + let x-norm = (vec.at(0) - angular.min) / (angular.max - angular.min) + let y-norm = (vec.at(1) - distal.min) / (distal.max - distal.min) + let theta = 2 * calc.pi * x-norm + let dist = (radius/2) * y-norm + let x = dist * calc.cos(theta) + let y = dist * calc.sin(theta) + + (radius/2 + x, radius/2 + y) +} + +// Draw inside viewport coordinates of two axes +// +// - size (vector): Axis canvas size (relative to origin) +// - x (axis): Horizontal axis +// - y (axis): Vertical axis +// - z (axis): Z axis +// - name (string,none): Group name +#let axis-viewport(size,(x, y,), 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(size, (x, y), pt) + })) + }) + } + if "pos" in d { + d.pos = transform-vec(size, (x, y), d.pos) + } + return d + }) + + return ( + ctx: ctx, + drawables: drawable.apply-transform(ctx.transform, drawables) + ) + },)) +} + +#let data-viewport((x, y), size, body, name: none) = { + if body == none or body == () { return } + + assert.ne(x.horizontal, y.horizontal, + message: "Data must use one horizontal and one vertical axis!") + + // If y is the horizontal axis, swap x and y + // coordinates by swapping the transformation + // matrix columns. + if y.horizontal { + (x, y) = (y, x) + body = draw.set-ctx(ctx => { + ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) + return ctx + }) + body + } + + // Setup the viewport + axis-viewport(size, (x,y), body, name: name) +} \ No newline at end of file diff --git a/src/plot/bar.typ b/src/plot/bar.typ deleted file mode 100644 index 2c40233..0000000 --- a/src/plot/bar.typ +++ /dev/null @@ -1,264 +0,0 @@ -#import "/src/cetz.typ": draw, util - -#import "errorbar.typ": draw-errorbar - -#let _transform-row(row, x-key, y-key, error-key) = { - let x = row.at(x-key) - let y = if y-key == auto { - row.slice(1) - } else if type(y-key) == array { - y-key.map(k => row.at(k, default: 0)) - } else { - row.at(y-key, default: 0) - } - let err = if error-key == none { - 0 - } else if type(error-key) == array { - error-key.map(k => row.at(k, default: 0)) - } else { - row.at(error-key, default: 0) - } - - if type(y) != array { y = (y,) } - if type(err) != array { err = (err,) } - - (x, y.flatten(), err.flatten()) -} - -// Get a single items min and maximum y-value -#let _minmax-value(row) = { - let min = none - let max = none - - let y = row.at(1) - let e = row.at(2) - for i in range(0, y.len()) { - let i-min = y.at(i) - e.at(i, default: 0) - if min == none { min = i-min } - else { min = calc.min(min, i-min) } - - let i-max = y.at(i) + e.at(i, default: 0) - if max == none { max = i-max } - else { max = calc.max(max, i-max) } - } - - return (min: min, max: max) -} - -// Functions for max value calculation -#let _max-value-fn = ( - basic: (data, min: 0) => { - calc.max(min, ..data.map(t => _minmax-value(t).max)) - }, - clustered: (data, min: 0) => { - calc.max(min, ..data.map(t => _minmax-value(t).max)) - }, - stacked: (data, min: 0) => { - calc.max(min, ..data.map(t => t.at(1).sum())) - }, - stacked100: (.., min: 0) => {min + 100} -) - -// Functions for min value calculation -#let _min-value-fn = ( - basic: (data, min: 0) => { - calc.min(min, ..data.map(t => _minmax-value(t).min)) - }, - clustered: (data, min: 0) => { - calc.min(min, ..data.map(t => _minmax-value(t).min)) - }, - stacked: (data, min: 0) => { - calc.min(min, ..data.map(t => t.at(1).sum())) - }, - stacked100: (.., min: 0) => {min} -) - -#let _prepare(self, ctx) = { - return self -} - -#let _get-x-offset(position, width) = { - if position == "start" { 0 } - else if position == "end" { width } - else { width / 2 } -} - -#let _draw-rects(filling, self, ctx, ..args) = { - let x-axis = ctx.x - let y-axis = ctx.y - - let bars = () - let errors = () - - let w = self.bar-width - for d in self.data { - let (x, n, len, y-min, y-max, err) = d - - let w = self.bar-width - let gap = self.cluster-gap * if w > 0 { -1 } else { +1 } - w += gap * (len - 1) - - let x-offset = _get-x-offset(self.bar-position, self.bar-width) - x-offset += gap * n - - let left = x - x-offset - let right = left + w - let width = (right - left) / len - - if self.mode in ("basic", "clustered") { - left = left + width * n - right = left + width - } - - if (left <= x-axis.max and right >= x-axis.min and - y-min <= y-axis.max and y-max >= y-axis.min) { - left = calc.max(left, x-axis.min) - right = calc.min(right, x-axis.max) - y-min = calc.max(y-min, y-axis.min) - y-max = calc.min(y-max, y-axis.max) - - draw.rect((left, y-min), (right, y-max)) - - if not filling and err != 0 { - let y-whisker-size = self.whisker-size * ctx.x-scale - draw-errorbar(((left + right) / 2, y-max), - 0, err, 0, y-whisker-size / 2, self.style + self.error-style) - } - } - } -} - -#let _stroke(self, ctx) = { - _draw-rects(false, self, ctx, fill: none) -} - -#let _fill(self, ctx) = { - _draw-rects(true, self, ctx, stroke: none) -} - -/// Add a bar- or column-chart to the plot -/// -/// A bar- or column-chart is a chart where values are drawn as rectangular boxes. -/// -/// - data (array): Array of data items. An item is an array containing a x an one or more y values. -/// For example `(0, 1)` or `(0, 10, 5, 30)`. Depending on the `mode`, the data items -/// get drawn as either clustered or stacked rects. -/// - x-key: (int,string): Key to use for retreiving a bars x-value from a single data entry. -/// This value gets passed to the `.at(...)` function of a data item. -/// - y-key: (auto,int,string,array): Key to use for retreiving a bars y-value. For clustered/stacked -/// data, this must be set to a list of keys (e.g. `range(1, 4)`). If set to `auto`, att but the first -/// array-values of a data item are used as y-values. -/// - error-key: (none,int,string): Key to use for retreiving a bars y-error. -/// - mode (string): The mode on how to group data items into bars: -/// / basic: Add one bar per data value. If the data contains multiple values, -/// group those bars next to each other. -/// / clustered: Like "basic", but take into account the maximum number of values of all items -/// and group each cluster of bars together having the width of the widest cluster. -/// / stacked: Stack bars of subsequent item values onto the previous bar, generating bars -/// with the height of the sume of all an items values. -/// / stacked100: Like "stacked", but scale each bar to height $100$, making the different -/// bars percentages of the sum of an items values. -/// - labels (none,content,array): A single legend label for "basic" bar-charts, or a -/// a list of legend labels per bar category, if the mode is one of "clustered", "stacked" or "stacked100". -/// - bar-width (float): Width of one data item on the y axis -/// - bar-position (string): Positioning of data items relative to their x value. -/// - "start": The lower edge of the data item is on the x value (left aligned) -/// - "center": The data item is centered on the x value -/// - "end": The upper edge of the data item is on the x value (right aligned) -/// - cluster-gap (float): Spacing between bars insides a cluster. -/// - style (dictionary): Plot style -/// - axes (axes): Plot axes. To draw a horizontal growing bar chart, you can swap the x and y axes. -#let add-bar(data, - x-key: 0, - y-key: auto, - error-key: none, - mode: "basic", - labels: none, - bar-width: 1, - bar-position: "center", - cluster-gap: 0, - whisker-size: .25, - error-style: (:), - style: (:), - axes: ("x", "y")) = { - assert(mode in ("basic", "clustered", "stacked", "stacked100"), - message: "Mode must be basic, clustered, stacked or stacked100, but is " + mode) - assert(bar-position in ("start", "center", "end"), - message: "Invalid bar-position '" + bar-position + "'. Allowed values are: start, center, end") - assert(bar-width != 0, - message: "Option bar-width must be != 0, but is " + str(bar-width)) - if error-key != none { - assert(y-key != auto, - message: "Bar value-key must be set != auto if error-key is set") - assert(mode in ("basic", "clustered"), - message: "Error bars are supported for basic or clustered only, got " + mode) - } - - // Transform data to (x, y, error) triplets - let data = data.map(row => _transform-row(row, x-key, y-key, error-key)) - - let n = util.max(..data.map(d => d.at(1).len())) - let x-offset = _get-x-offset(bar-position, bar-width) - let x-domain = (util.min(..data.map(d => d.at(0))) - x-offset, - util.max(..data.map(d => d.at(0))) - x-offset + bar-width) - let y-domain = (_min-value-fn.at(mode)(data), - _max-value-fn.at(mode)(data)) - - // For stacked 100%, multiply each column/bar - if mode == "stacked100" { - data = data.map(((x, y, err)) => { - let f = 100 / y.sum() - return (x, y.map(v => v * f), err) - }) - } - - // Transform data from (x, ..y) to (x, n, len, y-min, y-max) per y - let stacked = mode in ("stacked", "stacked100") - let clustered = mode == "clustered" - let bar-data = if mode == "basic" { - range(0, data.len()).map(_ => ()) - } else { - range(0, n).map(_ => ()) - } - - let j = 0 - for (x, y, err) in data { - let len = if clustered { n } else { y.len() } - let sum = 0 - for (i, y) in y.enumerate() { - let err = err.at(i, default: 0) - if stacked { - bar-data.at(i).push((x, i, len, sum, sum + y, err)) - } else if clustered { - bar-data.at(i).push((x, i, len, 0, y, err)) - } else { - bar-data.at(j).push((x, i, len, 0, y, err)) - } - sum += y - } - j += 1 - } - - let labels = if type(labels) == array { labels } else { (labels,) } - range(0, bar-data.len()).map(i => ( - type: "bar", - label: labels.at(i, default: none), - axes: axes, - mode: mode, - data: bar-data.at(i), - x-domain: x-domain, - y-domain: y-domain, - style: style, - bar-width: bar-width, - bar-position: bar-position, - cluster-gap: cluster-gap, - whisker-size: whisker-size, - error-style: error-style, - plot-prepare: _prepare, - plot-stroke: _stroke, - plot-fill: _fill, - plot-legend-preview: self => { - draw.rect((0,0), (1,1), ..self.style) - } - )) -} diff --git a/src/plot/boxwhisker.typ b/src/plot/boxwhisker.typ deleted file mode 100644 index 9b34c5e..0000000 --- a/src/plot/boxwhisker.typ +++ /dev/null @@ -1,117 +0,0 @@ -#import "/src/cetz.typ": draw, util - -/// Add one or more box or whisker plots -/// -/// #example(``` -/// cetz.plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// cetz.plot.add-boxwhisker((x: 1, // Location on x-axis -/// outliers: (7, 65, 69), // Optional outlier values -/// min: 15, max: 60, // Minimum and maximum -/// q1: 25, // Quartiles: Lower -/// q2: 35, // Median -/// q3: 50)) // Upper -/// }) -/// ```) -/// -/// - data (array, dictionary): dictionary or array of dictionaries containing the -/// needed entries to plot box and whisker plot. -/// -/// The following fields are supported: -/// - `x` (number) X-axis value -/// - `min` (number) Minimum value -/// - `max` (number) Maximum value -/// - `q1`, `q2`, `q3` (number) Quartiles from lower to to upper -/// - `outliers` (array of number) Optional outliers -/// -/// - axes (array): Name of the axes to use ("x", "y"), note that not all -/// plot styles are able to display a custom axis! -/// - style (style): Style to use, can be used with a palette function -/// - box-width (float): Width from edge-to-edge of the box of the box and whisker in plot units. Defaults to 0.75 -/// - whisker-width (float): Width from edge-to-edge of the whisker of the box and whisker in plot units. Defaults to 0.5 -/// - mark (string): Mark to use for plotting outliers. Set `none` to disable. Defaults to "x" -/// - mark-size (float): Size of marks for plotting outliers. Defaults to 0.15 -/// - label (none,content): Legend label to show for this plot. -#let add-boxwhisker(data, - label: none, - axes: ("x", "y"), - style: (:), - box-width: 0.75, - whisker-width: 0.5, - mark: "*", - mark-size: 0.15) = { - // Add multiple boxes as multiple calls to - // add-boxwhisker - if type(data) == array { - for it in data { - add-boxwhisker( - it, - axes:axes, - style: style, - box-width: box-width, - whisker-width: whisker-width, - mark: mark, - mark-size: mark-size) - } - return - } - - assert("x" in data, message: "Specify 'x', the x value at which to display the box and whisker") - assert("q1" in data, message: "Specify 'q1', the lower quartile") - assert("q2" in data, message: "Specify 'q2', the median") - assert("q3" in data, message: "Specify 'q3', the upper quartile") - assert("min" in data, message: "Specify 'min', the minimum excluding outliers") - assert("max" in data, message: "Specify 'max', the maximum excluding outliers") - assert(data.q1 <= data.q2 and data.q2 <= data.q3, - message: "The quartiles q1, q2 and q3 must follow q1 < q2 < q3") - assert(data.min <= data.q1 and data.max >= data.q2, - message: "The minimum and maximum must be <= q1 and >= q3") - - // Y domain - let max-value = util.max(data.max, ..data.at("outliers", default: ())) - let min-value = util.min(data.min, ..data.at("outliers", default: ())) - - let prepare(self, ctx) = { - return self - } - - let stroke(self, ctx) = { - let data = self.bw-data - - // Box - draw.rect((data.x - box-width / 2, data.q1), - (data.x + box-width / 2, data.q3), - ..self.style) - - // Mean - draw.line((data.x - box-width / 2, data.q2), - (data.x + box-width / 2, data.q2), - ..self.style) - - // whiskers - let whisker(x, start, end) = { - draw.line((x, start),(x, end),..self.style) - draw.line((x - whisker-width / 2, end),(x + whisker-width / 2, end), ..self.style) - } - whisker(data.x, data.q3, data.max) - whisker(data.x, data.q1, data.min) - } - - (( - type: "boxwhisker", - label: label, - axes: axes, - bw-data: data, - style: style, - plot-prepare: prepare, - plot-stroke: stroke, - x-domain: (data.x - calc.max(whisker-width, box-width), - data.x + calc.max(whisker-width, box-width)), - y-domain: (min-value, max-value), - ) + (if "outliers" in data { ( - type: "boxwhisker-outliers", - data: data.outliers.map(it => (data.x, it)), - mark: mark, - mark-size: mark-size, - mark-style: (:) - ) }),) -} diff --git a/src/plot/contour.typ b/src/plot/contour.typ deleted file mode 100644 index db611c9..0000000 --- a/src/plot/contour.typ +++ /dev/null @@ -1,350 +0,0 @@ -#import "/src/cetz.typ": draw - -#import "util.typ" -#import "sample.typ" - -// Find contours of a 2D array by using marching squares algorithm -// -// - data (array): A 2D array of floats where the first index is the row and the second index is the column -// - offset (float): Z value threshold of a cell compare with `op` to, to count as true -// - op (auto,string,function): Z value comparison oparator: -// / `">", ">=", "<", "<=", "!=", "=="`: Use the passed operator to compare z. -// / `auto`: Use ">=" for positive z values, "<=" for negative z values. -// / ``: If set to a function, that function gets called -// with two arguments, the z value `z1` to compare against and -// the z value `z2` of the data and must return a boolean: `(z1, z2) => boolean`. -// - interpolate (bool): Enable cell interpolation for smoother lines -// - contour-limit (int): Contour limit after which the algorithm panics -// -> array: Array of contour point arrays -#let find-contours(data, offset, op: auto, interpolate: true, contour-limit: 50) = { - assert(data != none and type(data) == array, - message: "Data must be of type array") - assert(type(offset) in (int, float), - message: "Offset must be numeric") - - let n-rows = data.len() - let n-cols = data.at(0).len() - if n-rows < 2 or n-cols < 2 { - return () - } - - assert(op == auto or type(op) in (str, function), - message: "Operator must be of type auto, string or function") - if op == auto { - op = if offset < 0 { "<=" } else { ">=" } - } - if type(op) == str { - assert(op in ("<", "<=", ">", ">=", "==", "!="), - message: "Operator must be one of: <, <=, >, >=, != or ==") - } - - // Return if data is set - let is-set = if type(op) == function { - v => op(offset, v) - } else if op == "==" { - v => v == offset - } else if op == "!=" { - v => v != offset - } else if op == "<" { - v => v < offset - } else if op == "<=" { - v => v <= offset - } else if op == ">" { - v => v > offset - } else if op == ">=" { - v => v >= offset - } - - // Build a binary map that has 0 for unset and 1 for set cells - let bin-data = data.map(r => r.map(is-set)) - - // Get binary data at x, y - let get-bin(x, y) = { - if x >= 0 and x < n-cols and y >= 0 and y < n-rows { - return bin-data.at(y).at(x) - } - return false - } - - // Get data point for x, y coordinate - let get-data(x, y) = { - if x >= 0 and x < n-cols and y >= 0 and y < n-rows { - return float(data.at(y).at(x)) - } - return none - } - - // Get case (0 to 15) - let get-case(tl, tr, bl, br) = { - int(tl) * 8 + int(tr) * 4 + int(br) * 2 + int(bl) - } - - let lerp(a, b) = { - if a == b { return a } - else if a == none { return 1 } - else if b == none { return 0 } - return (offset - a) / (b - a) - } - - // List of all found contours - let contours = () - - let segments = () - for y in range(-1, n-rows) { - for x in range(-1, n-cols) { - let tl = get-bin(x, y) - let tr = get-bin(x+1, y) - let bl = get-bin(x, y+1) - let br = get-bin(x+1, y+1) - - // Corner data - // - // nw-----ne - // | | - // | | - // | | - // sw-----se - let nw = get-data(x, y) - let ne = get-data(x+1, y) - let se = get-data(x+1, y+1) - let sw = get-data(x, y+1) - - // Interpolated edge points - // - // +-- a --+ - // | | - // d b - // | | - // +-- c --+ - let a = (x + .5, y) - let b = (x + 1, y + .5) - let c = (x + .5, y + 1) - let d = (x, y + .5) - if interpolate { - a = (x + lerp(nw, ne), y) - b = (x + 1, y + lerp(ne, se)) - c = (x + lerp(sw, se), y + 1) - d = (x, y + lerp(nw, sw)) - } - - let case = get-case(tl, tr, bl, br) - if case in (1, 14) { - segments.push((d, c)) - } else if case in (2, 13) { - segments.push((b, c)) - } else if case in (3, 12) { - segments.push((d, b)) - } else if case in (4, 11) { - segments.push((a, b)) - } else if case == 5 { - segments.push((d, a)) - segments.push((c, b)) - } else if case in (6, 9) { - segments.push((c, a)) - } else if case in (7, 8) { - segments.push((d, a)) - } else if case == 10 { - segments.push((a, b)) - segments.push((c, d)) - } - } - } - - // Join lines to one or more contours - // This is done by searching for the next line - // that starts at the current contours head or tail - // point. If found, push the other coordinate to - // the contour. If no line could be found, push a - // new contour. - let contours = () - while segments.len() > 0 { - if contours.len() == 0 { - contours.push(segments.remove(0)) - } - - let found = false - - let i = 0 - while i < segments.len() { - let (a, b) = segments.at(i) - let (h, t) = (contours.last().first(), - contours.last().last()) - if a == t { - contours.last().push(b) - segments.remove(i) - found = true - } else if b == t { - contours.last().push(a) - segments.remove(i) - found = true - } else if a == h { - contours.last().insert(0, b) - segments.remove(i) - found = true - } else if b == h { - contours.last().insert(0, a) - segments.remove(i) - found = true - } else { - i += 1 - } - } - - // Insert the next contour - if not found { - contours.push(segments.remove(0)) - } - - // Check limit - assert(contours.len() <= contour-limit, - message: "Countour limit reached! Raise contour-limit if you " + - "think this is not an error") - } - - return contours -} - -// Prepare line data -#let _prepare(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - - self.contours = self.contours.map(c => { - c.stroke-paths = util.compute-stroke-paths(c.line-data, - (x.min, y.min), (x.max, y.max)) - - if self.fill { - c.fill-paths = util.compute-fill-paths(c.line-data, - (x.min, y.min), (x.max, y.max)) - } - return c - }) - - return self -} - -// Stroke line data -#let _stroke(self, ctx) = { - for c in self.contours { - for p in c.stroke-paths { - draw.line(..p, fill: none, close: p.first() == p.last()) - } - } -} - -// Fill line data -#let _fill(self, ctx) = { - if not self.fill { return } - for c in self.contours { - for p in c.fill-paths { - draw.line(..p, stroke: none, close: p.first() == p.last()) - } - } -} - -/// Add a contour plot of a sampled function or a matrix. -/// -/// #example(``` -/// cetz.plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// cetz.plot.add-contour(x-domain: (-3, 3), y-domain: (-3, 3), -/// style: (fill: rgb(50,50,250,50)), -/// fill: true, -/// op: "<", // Find contours where data < z -/// z: (2.5, 2, 1), // Z values to find contours for -/// (x, y) => calc.sqrt(x * x + y * y)) -/// }) -/// ```) -/// -/// - data (array, function): A function of the signature `(x, y) => z` -/// or an array of arrays of floats (a matrix) where the first -/// index is the row and the second index is the column. -/// - z (float, array): Z values to plot. Contours containing values -/// above z (z >= 0) or below z (z < 0) get plotted. -/// If you specify multiple z values, they get plotted in the order of specification. -/// - x-domain (domain): X axis domain used if `data` is a function, that is the -/// domain inside the function gets sampled. -/// - y-domain (domain): Y axis domain used if `data` is a function, see `x-domain`. -/// - x-samples (int): X axis domain samples (2 < n). Note that contour finding -/// can be quite slow. Using a big sample count can improve accuracy but can -/// also lead to bad compilation performance. -/// - y-samples (int): Y axis domain samples (2 < n) -/// - interpolate (bool): Use linear interpolation between sample values which can -/// improve the resulting plot, especially if the contours are curved. -/// - op (auto,string,function): Z value comparison oparator: -/// / `">", ">=", "<", "<=", "!=", "=="`: Use the operator for comparison of `z` to -/// the values from `data`. -/// / `auto`: Use ">=" for positive z values, "<=" for negative z values. -/// / ``: Call comparison function of the format `(plot-z, data-z) => boolean`, -/// where `plot-z` is the z-value from the plots `z` argument and `data-z` -/// is the z-value of the data getting plotted. The function must return true -/// if at the combinations of arguments a contour is detected. -/// - fill (bool): Fill each contour -/// - style (style): Style to use for plotting, can be used with a palette function. Note -/// that all z-levels use the same style! -/// - axes (axes): Name of the axes to use for plotting. -/// - limit (int): Limit of contours to create per z value before the function panics -/// - label (none,content): Plot legend label to show. The legend preview for -/// contour plots is a little rectangle drawn with the contours style. -#let add-contour(data, - label: none, - z: (1,), - x-domain: (0, 1), - y-domain: (0, 1), - x-samples: 25, - y-samples: 25, - interpolate: true, - op: auto, - axes: ("x", "y"), - style: (:), - fill: false, - limit: 50, - ) = { - // Sample a x/y function - if type(data) == function { - data = sample.sample-fn2(data, - x-domain, y-domain, - x-samples, y-samples) - } - - // Find matrix dimensions - assert(type(data) == array) - let (x-min, x-max) = x-domain - let dx = (x-max - x-min) / (data.at(0).len() - 1) - let (y-min, y-max) = y-domain - let dy = (y-max - y-min) / (data.len() - 1) - - let contours = () - let z = if type(z) == array { z } else { (z,) } - for z in z { - for contour in find-contours(data, z, op: op, interpolate: interpolate, contour-limit: limit) { - let line-data = contour.map(pt => { - (pt.at(0) * dx + x-min, - pt.at(1) * dy + y-min) - }) - - contours.push(( - z: z, - line-data: line-data, - )) - } - } - - return (( - type: "contour", - label: label, - contours: contours, - axes: axes, - x-domain: x-domain, - y-domain: y-domain, - style: style, - fill: fill, - mark: none, - mark-style: none, - plot-prepare: _prepare, - plot-stroke: _stroke, - plot-fill: _fill, - plot-legend-preview: self => { - if not self.fill { self.style.fill = none } - draw.rect((0,0), (1,1), ..self.style) - } - ),) -} diff --git a/src/plot/elements/anchor.typ b/src/plot/elements/anchor.typ new file mode 100644 index 0000000..604d179 --- /dev/null +++ b/src/plot/elements/anchor.typ @@ -0,0 +1,34 @@ +/// Add an anchor to a plot environment +/// +/// This function is similar to `draw.anchor` but it takes an additional +/// axis tuple to specify which axis coordinate system to use. +/// +/// #example(``` +/// import cetz.plot +/// import cetz.draw: * +/// plot.plot(size: (2,2), name: "plot", +/// x-tick-step: none, y-tick-step: none, { +/// plot.add(((0,0), (1,1), (2,.5), (4,3))) +/// plot.add-anchor("pt", (1,1)) +/// }) +/// +/// line("plot.pt", ((), "|-", (0,1.5)), mark: (start: ">"), name: "line") +/// content("line.end", [Here], anchor: "south", padding: .1) +/// ```) +/// +/// - name (string): Anchor name +/// - position (tuple): Tuple of x and y values. +/// Both values can have the special values "min" and +/// "max", which resolve to the axis min/max value. +/// Position is in axis space defined by the axes passed to `axes`. +/// - axes (tuple): Name of the axes to use `("x", "y")` as coordinate +/// system for `position`. Note that both axes must be used, +/// as `add-anchors` does not create them on demand. +#let anchor(name, position, axes: ("x", "y")) = { + (( + type: "anchor", + name: name, + position: position, + axes: axes, + ),) +} \ No newline at end of file diff --git a/src/plot/annotation.typ b/src/plot/elements/annotation.typ similarity index 91% rename from src/plot/annotation.typ rename to src/plot/elements/annotation.typ index 19fb2e5..1dd22c9 100644 --- a/src/plot/annotation.typ +++ b/src/plot/elements/annotation.typ @@ -1,7 +1,4 @@ -#import "/src/cetz.typ" -#import cetz: draw, process, util, matrix -#import "util.typ" -#import "sample.typ" +#import "/src/cetz.typ" as cetz: draw, process, util, matrix /// Add an annotation to the plot /// @@ -31,7 +28,7 @@ /// adjustment /// - background (bool): If true, the annotation is drawn behind all plots, in the background. /// If false, the annotation is drawn above all plots. -#let annotate(body, axes: ("x", "y"), resize: true, padding: none, background: false) = { +#let annotation(body, axes: ("x", "y"), resize: true, padding: none, background: false) = { (( type: "annotation", body: { @@ -73,4 +70,4 @@ y.max = calc.max(y.max, y-max) return (x, y) -} +} \ No newline at end of file diff --git a/src/plot/errorbar.typ b/src/plot/elements/errorbar.typ similarity index 61% rename from src/plot/errorbar.typ rename to src/plot/elements/errorbar.typ index e8ec68c..9100efb 100644 --- a/src/plot/errorbar.typ +++ b/src/plot/elements/errorbar.typ @@ -50,10 +50,17 @@ let x-whisker-size = self.whisker-size * ctx.y-scale let y-whisker-size = self.whisker-size * ctx.x-scale - draw-errorbar((self.x, self.y), - self.x-error, self.y-error, - x-whisker-size, y-whisker-size, - self.style) + for d in self.data { + draw-errorbar( + (d.at(self.x-key),d.at(self.y-key)), + if self.x-error-key != none {d.at(self.x-error-key, default: 0)} else {0}, + if self.y-error-key != none {d.at(self.y-error-key, default: 0)} else {0}, + x-whisker-size, + y-whisker-size, + self.style + ) + } + } /// Add x- and/or y-error bars @@ -68,51 +75,62 @@ /// - style (dictionary): Style for the error bars /// - label: (none,content): Label to tsh /// - axes (axes): Plot axes. To draw a horizontal growing bar chart, you can swap the x and y axes. -#let add-errorbar(pt, - x-error: 0, - y-error: 0, - label: none, - mark: "o", - mark-size: .2, - mark-style: (:), - whisker-size: .5, - style: (:), - axes: ("x", "y")) = { - assert(x-error != 0 or y-error != 0, - message: "Either x-error or y-error must be set.") - - let (x, y) = pt - - if type(x-error) != array { - x-error = (x-error, x-error) - } - if type(y-error) != array { - y-error = (y-error, y-error) - } +#let errorbar(data, + x-key: 0, + y-key: 1, + x-error-key: none, + y-error-key: none, + x-error: 0, + y-error: 0, + label: none, + whisker-size: .2, + style: (:), + axes: ("x", "y")) = { + assert(x-error-key != none or y-error-key != none, + message: "Either x-error-key or y-error-key must be set.") + + // x-error.at(0) = calc.abs(x-error.at(0)) * -1 + // y-error.at(0) = calc.abs(y-error.at(0)) * -1 - x-error.at(0) = calc.abs(x-error.at(0)) * -1 - y-error.at(0) = calc.abs(y-error.at(0)) * -1 + let x-domain = if x-error-key != none { + ( + calc.min(..data.map(it=>(it.at(x-key)-it.at(x-error-key)))), + calc.max(..data.map(it=>(it.at(x-key)+it.at(x-error-key)))) + ) + } - let x-domain = x-error.map(v => v + x) - let y-domain = y-error.map(v => v + y) + let y-domain = if y-error-key != none { + ( + calc.min(..data.map(it=>(it.at(y-key)-it.at(y-error-key)))), + calc.max(..data.map(it=>(it.at(y-key)+it.at(y-error-key)))) + ) + } return (( type: "errorbar", label: label, axes: axes, - data: ((x,y),), - x: x, - y: y, - x-error: x-error, - y-error: y-error, + + data: data, + x-key: x-key, + y-key: y-key, + x-error-key: x-error-key, + y-error-key: y-error-key, + x-domain: x-domain, y-domain: y-domain, - mark: mark, - mark-size: mark-size, - mark-style: mark-style, + whisker-size: whisker-size, style: style, plot-prepare: _prepare, plot-stroke: _stroke, + plot-legend-preview: (self) => { + draw-errorbar( + (0.5, 0.5), + 0, 0.4, + 0.01, 0.1, + self.style + ) + } ),) -} +} \ No newline at end of file diff --git a/src/plot/elements/series.typ b/src/plot/elements/series.typ new file mode 100644 index 0000000..8dd11df --- /dev/null +++ b/src/plot/elements/series.typ @@ -0,0 +1,96 @@ +#import "/src/cetz.typ": draw +#import "/src/plot/mark.typ" + +#let _prepare(self, ctx) = { + for (key, value) in self.body.enumerate() { + value.style = self.style + value.style + self.body.at(key) = (value.plot-prepare)(value, ctx) + } + return self +} + +#let _stroke(self, ctx) = { + for (key, value) in self.body.enumerate() { + if "plot-stroke" in value { + (value.plot-stroke)(value, ctx) + } + + if "mark" in value and value.mark != none { + // draw.group({ + // draw.set-style(..value.style, ..value.mark-style) + // mark.draw-mark(value.data, ctx, value.mark, value.mark-size, ) + // }) + } + } +} + +#let _fill(self, ctx) = { + for (key, value) in self.body.enumerate() { + if not "plot-fill" in value {continue} + (value.plot-fill)(value, ctx) + } +} + +#let _legend-preview(self) = { + for (key, value) in self.body.enumerate() { + if not "plot-legend-preview" in value {continue} + (value.plot-legend-preview)(value) + } +} + + +#let series( + body, + label: none, + style: (:), + axes: ("x", "y") +) = { + + let x-domain = ( + calc.min( + ..body.map(it=>{ + if ("x-domain" in it) and (it.x-domain != none) { + it.x-domain.at(0) + } else {0} + }) + ), + calc.max( + ..body.map(it=>{ + if ("x-domain" in it) and (it.x-domain != none) { + it.x-domain.at(1) + } else {0} + }) + ), + ) + + let y-domain = ( + calc.min( + ..body.map(it=>{ + if ("y-domain" in it) and (it.y-domain != none) { + it.y-domain.at(0) + } else {0} + }) + ), + calc.max( + ..body.map(it=>{ + if ("y-domain" in it) and (it.y-domain != none) { + it.y-domain.at(1) + } else {0} + }) + ), + ) + + (( + type: "series", + label: label, + body: body, + axes: axes, + style: style, + x-domain: x-domain, + y-domain: y-domain, + plot-prepare: _prepare, + plot-stroke: _stroke, + plot-fill: _fill, + plot-legend-preview: _legend-preview, + ),) +} \ No newline at end of file diff --git a/src/plot/violin.typ b/src/plot/elements/violin.typ similarity index 95% rename from src/plot/violin.typ rename to src/plot/elements/violin.typ index 3f8f876..726a93c 100644 --- a/src/plot/violin.typ +++ b/src/plot/elements/violin.typ @@ -1,6 +1,5 @@ #import "/src/cetz.typ": draw -#import "util.typ" -#import "sample.typ" +#import "/src/plot/sample.typ" #let kernel-normal(x, stdev: 1.5) = { (1/calc.sqrt(2*calc.pi*calc.pow(stdev,2))) * calc.exp( - (x*x)/(2*calc.pow(stdev,2))) @@ -20,8 +19,8 @@ path = path.map( ((x,y))=>{(2 * violin.x-position - x,y)}) } - let (x, y) = (ctx.x, ctx.y) - let stroke-paths = util.compute-stroke-paths(path, (x.min, y.min), (x.max, y.max)) + let (x, y) = (ctx.axes.at(0), ctx.axes.at(1)) + let stroke-paths = (ctx.compute-stroke-paths)(path, ctx) for p in stroke-paths{ let args = arguments(..p, closed: self.side == "both") @@ -93,7 +92,7 @@ /// - mark-style (dictionary): (unused, will eventually be used to render interquartile ranges). /// - axes (axes): (unstable, documentation to follow once completed). /// - label (none, content): The name of the category to be shown in the legend. -#let add-violin( +#let violin( data, x-key: 0, y-key: 1, diff --git a/src/plot/line.typ b/src/plot/elements/xy.typ similarity index 92% rename from src/plot/line.typ rename to src/plot/elements/xy.typ index 12b5c6c..092623c 100644 --- a/src/plot/line.typ +++ b/src/plot/elements/xy.typ @@ -1,7 +1,7 @@ #import "/src/cetz.typ": draw -#import "util.typ" -#import "sample.typ" +#import "/src/plot/util.typ" +#import "/src/plot/sample.typ" // Transform points // @@ -83,19 +83,23 @@ // Prepare line data #let _prepare(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) + let (x, y) = (ctx.axes.at(0), ctx.axes.at(1)) // Generate stroke paths - self.stroke-paths = util.compute-stroke-paths(self.line-data, - (x.min, y.min), (x.max, y.max)) + self.stroke-paths = (ctx.compute-stroke-paths)( + self.line-data, + ctx, + ) // Compute fill paths if filling is requested self.hypograph = self.at("hypograph", default: false) self.epigraph = self.at("epigraph", default: false) self.fill = self.at("fill", default: false) if self.hypograph or self.epigraph or self.fill { - self.fill-paths = util.compute-fill-paths(self.line-data, - (x.min, y.min), (x.max, y.max)) + self.fill-paths = (ctx.compute-fill-paths)( + self.line-data, + ctx, + ) } return self @@ -103,16 +107,16 @@ // Stroke line data #let _stroke(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - - for p in self.stroke-paths { - draw.line(..p, fill: none) - } + if ("stroke" in self.style and self.style.stroke != none){ + for p in self.stroke-paths { + draw.line(..p, ..self.style, fill: none) + } + } } // Fill line data #let _fill(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) + let (x, y) = (ctx.axes.at(0), ctx.axes.at(1)) if self.hypograph { fill-segments-to(self.fill-paths, y.min) @@ -124,8 +128,10 @@ 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)) + fill-segments-to( + self.fill-paths, + calc.max(calc.min(y.max, 0), y.min) + ) } } } @@ -206,7 +212,7 @@ /// }) /// ```) /// - label (none,content): Legend label to show for this plot. -#let add(domain: auto, +#let xy(domain: auto, hypograph: false, epigraph: false, fill: false, @@ -220,13 +226,19 @@ line: "linear", axes: ("x", "y"), label: none, - data + + data, + x-key: 0, + y-key: 1, ) = { // If data is of type function, sample it if type(data) == function { data = sample.sample-fn(data, domain, samples, sample-at: sample-at) } + // data + let data = data.map(it=>(it.at(x-key), it.at(y-key))) + // Transform data let line-data = transform-lines(data, line) @@ -288,7 +300,7 @@ /// - axes (array): Name of the axes to use for plotting /// - style (style): Style to use, can be used with a palette function /// - label (none,content): Legend label to show for this plot. -#let add-hline(..y, +#let hline(..y, min: auto, max: auto, axes: ("x", "y"), @@ -350,7 +362,7 @@ /// plot styles are able to display a custom axis! /// - style (style): Style to use, can be used with a palette function /// - label (none,content): Legend label to show for this plot. -#let add-vline(..x, +#let vline(..x, min: auto, max: auto, axes: ("x", "y"), @@ -422,7 +434,7 @@ /// - axes (array): Name of the axes to use for plotting. /// - data-a (array,function): Data of the first plot, see @@add(). /// - data-b (array,function): Data of the second plot, see @@add(). -#let add-fill-between(data-a, +#let fill-between(data-a, data-b, domain: auto, samples: 50, @@ -460,24 +472,25 @@ )} let prepare(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) + let (x, y) = (ctx.axes.at(0), ctx.axes.at(1)) // Generate stroke paths self.stroke-paths = ( - a: util.compute-stroke-paths(self.line-data.a, - (x.min, y.min), (x.max, y.max)), - b: util.compute-stroke-paths(self.line-data.b, - (x.min, y.min), (x.max, y.max)) + a: (ctx.compute-stroke-paths)(self.line-data.a, ctx), + b: (ctx.compute-stroke-paths)(self.line-data.b, ctx) ) // Generate fill paths - self.fill-paths = util.compute-fill-paths(self.line-data.a + self.line-data.b.rev(), - (x.min, y.min), (x.max, y.max)) + self.fill-paths = (ctx.compute-fill-paths)( + self.line-data.a + self.line-data.b.rev(), + ctx + ) return self } let stroke(self, ctx) = { + if "stroke" not in self.style or self.style.stroke == none {return} for p in self.stroke-paths.a { draw.line(..p, fill: none) } @@ -505,4 +518,4 @@ draw.rect((0,0), (1,1), ..self.style) } ),) -} +} \ No newline at end of file diff --git a/src/plot/legend.typ b/src/plot/legend.typ index f26a43b..b6b3a20 100644 --- a/src/plot/legend.typ +++ b/src/plot/legend.typ @@ -10,16 +10,16 @@ layer: 1, // Legend layer fill: rgb(255,255,255,200), // Legend background stroke: black, // Legend border - padding: .1, // Legend border padding + padding: .125, // Legend border padding offset: (0, 0), // Legend displacement spacing: .1, // Spacing between anchor and legend item: ( radius: 0, - spacing: .05, // Spacing between items + spacing: .1, // Spacing between items preview: ( width: .75, // Preview width height: .3, // Preview height - margin: .1 // Distance between preview and label + margin: .15 // Distance between preview and label ) ), radius: 0, diff --git a/src/plot/mark.typ b/src/plot/mark.typ index 9450d21..129502e 100644 --- a/src/plot/mark.typ +++ b/src/plot/mark.typ @@ -1,5 +1,5 @@ #import "/src/cetz.typ": draw -#import "/src/axes.typ" +#import "/src/axes/axes.typ" // Draw mark at point with size #let draw-mark-shape(pt, size, mark, style) = { @@ -34,9 +34,9 @@ } } -#let draw-mark(pts, x, y, mark, mark-size, plot-size) = { +#let draw-mark(pts, ctx, mark, mark-size, plot-size) = { let pts = pts.map(pt => { - axes.transform-vec(plot-size, x, y, none, pt) + (ctx.transform-vec)(plot-size, ctx.axes, pt) }).filter(pt => pt != none) for pt in pts { diff --git a/src/plot/sample.typ b/src/plot/sample.typ index 3ad881d..e813256 100644 --- a/src/plot/sample.typ +++ b/src/plot/sample.typ @@ -76,4 +76,4 @@ return float((fn)(x, y)) }) }) -} +} \ No newline at end of file diff --git a/src/plot/styles.typ b/src/plot/styles.typ new file mode 100644 index 0000000..82172c7 --- /dev/null +++ b/src/plot/styles.typ @@ -0,0 +1,120 @@ +#import "/src/cetz.typ": util, styles + +/// 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, + 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: 2, + grid-layer: -1, + background-layer: -2, + padding: 0, + tick: ( + fill: none, + stroke: black + 0.75pt, + minor-stroke: black + .25pt, + 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(10%), thickness: .5pt), + ), + minor-grid: ( + stroke: (paint: gray.lighten(40%), thickness: .25pt), + ), +) + +#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 + if "break-point" in style { + style.break-point.width = res(style.break-point.width) + style.break-point.length = res(style.break-point.length) + } + + + // Padding + if "padding" in style { + 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) +} \ No newline at end of file diff --git a/src/plot/util.typ b/src/plot/util.typ index bff54fc..b6c3f3d 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -1,195 +1,6 @@ #import "/src/cetz.typ" #import cetz.util: bezier -/// Clip line-strip in rect -/// -/// - points (array): Array of vectors representing a line-strip -/// - low (vector): Lower clip-window coordinate -/// - high (vector): Upper clip-window coordinate -/// -> array List of line-strips representing the paths insides the clip-window -#let clipped-paths(points, low, high, fill: false) = { - let (min-x, max-x) = (calc.min(low.at(0), high.at(0)), - calc.max(low.at(0), high.at(0))) - let (min-y, max-y) = (calc.min(low.at(1), high.at(1)), - calc.max(low.at(1), high.at(1))) - - let in-rect(pt) = { - return (pt.at(0) >= min-x and pt.at(0) <= max-x and - pt.at(1) >= min-y and pt.at(1) <= max-y) - } - - let interpolated-end(a, b) = { - if in-rect(a) and in-rect(b) { - return b - } - - let (x1, y1, ..) = a - let (x2, y2, ..) = b - - if x2 - x1 == 0 { - return (x2, calc.min(max-y, calc.max(y2, min-y))) - } - - if y2 - y1 == 0 { - return (calc.min(max-x, calc.max(x2, min-x)), y2) - } - - let m = (y2 - y1) / (x2 - x1) - let n = y2 - m * x2 - - let x = x2 - let y = y2 - - y = calc.min(max-y, calc.max(y, min-y)) - x = (y - n) / m - - x = calc.min(max-x, calc.max(x, min-x)) - y = m * x + n - - return (x, y) - } - - // Append path to paths and return paths - // - // If path starts or ends with a vector of another part, merge those - // paths instead appending path as a new path. - let append-path(paths, path) = { - if path.len() <= 1 { - return paths - } - - let cmp(a, b) = { - return a.map(calc.round.with(digits: 8)) == b.map(calc.round.with(digits: 8)) - } - - let added = false - for i in range(0, paths.len()) { - let p = paths.at(i) - if cmp(p.first(), path.last()) { - paths.at(i) = path + p - added = true - } else if cmp(p.first(), path.first()) { - paths.at(i) = path.rev() + p - added = true - } else if cmp(p.last(), path.first()) { - paths.at(i) = p + path - added = true - } else if cmp(p.last(), path.last()) { - paths.at(i) = p + path.rev() - added = true - } - if added { break } - } - - if not added { - paths.push(path) - } - return paths - } - - let clamped-pt(pt) = { - return (calc.max(min-x, calc.min(pt.at(0), max-x)), - calc.max(min-y, calc.min(pt.at(1), max-y))) - } - - let paths = () - - let path = () - let prev = points.at(0) - let was-inside = in-rect(prev) - if was-inside { - path.push(prev) - } else if fill { - path.push(clamped-pt(prev)) - } - - for i in range(1, points.len()) { - let prev = points.at(i - 1) - let pt = points.at(i) - - let is-inside = in-rect(pt) - - let (x1, y1) = prev - let (x2, y2) = pt - - // Ignore lines if both ends are outsides the x-window and on the - // same side. - if (x1 < min-x and x2 < min-x) or (x1 > max-x and x2 > max-x) { - if fill { - let clamped = clamped-pt(pt) - if path.last() != clamped { - path.push(clamped) - } - } - was-inside = false - continue - } - - if is-inside { - if was-inside { - path.push(pt) - } else { - path.push(interpolated-end(pt, prev)) - path.push(pt) - } - } else { - if was-inside { - path.push(interpolated-end(prev, pt)) - } else { - let (a, b) = (interpolated-end(pt, prev), - interpolated-end(prev, pt)) - if in-rect(a) and in-rect(b) { - path.push(a) - path.push(b) - } else if fill { - let clamped = clamped-pt(pt) - if path.last() != clamped { - path.push(clamped) - } - } - } - - if path.len() > 0 and not fill { - paths = append-path(paths, path) - path = () - } - } - - was-inside = is-inside - } - - // Append clamped last point if filling - if fill and not in-rect(points.last()) { - path.push(clamped-pt(points.last())) - } - - if path.len() > 1 { - paths = append-path(paths, path) - } - - return paths -} - -/// Compute clipped stroke paths -/// -/// - points (array): X/Y data points -/// - low (vector): Lower clip-window coordinate -/// - high (vector): Upper clip-window coordinate -/// -> array List of stroke paths -#let compute-stroke-paths(points, low, high) = { - clipped-paths(points, low, high, fill: false) -} - -/// Compute clipped fill path -/// -/// - points (array): X/Y data points -/// - low (vector): Lower clip-window coordinate -/// - high (vector): Upper clip-window coordinate -/// -> array List of fill paths -#let compute-fill-paths(points, low, high) = { - clipped-paths(points, low, high, fill: true) -} - /// Return points of a sampled catmull-rom through the /// input points. /// @@ -369,4 +180,4 @@ } return axis-dict -} +} \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore index fe5a11a..8c1e6be 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,3 @@ **/out/* **/diff/* -*.pdf +*.pdf \ No newline at end of file diff --git a/tests/axes/log-mode/ref/1.png b/tests/axes/log-mode/ref/1.png deleted file mode 100644 index 6c6d2af..0000000 Binary files a/tests/axes/log-mode/ref/1.png and /dev/null differ diff --git a/tests/axes/log-mode/test.typ b/tests/axes/log-mode/test.typ deleted file mode 100644 index f732959..0000000 --- a/tests/axes/log-mode/test.typ +++ /dev/null @@ -1,166 +0,0 @@ - - -#set page(width: auto, height: auto) - -#import "/tests/helper.typ": * -#import "/src/lib.typ": * -#import cetz: draw, canvas -#import cetz-plot: axes, - -// plot.add test with logarithmic scaling -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot( - size: (9, 6), - axis-style: "scientific", - y-mode: "log", y-base: 10, - y-format: "sci", - x-min: 1, x-max: 10, x-tick-step: 1, - y-min: 1, y-max: 10000, y-tick-step: 1, y-minor-tick-step: 1, - x-grid: "both", - y-grid: "both", - { - plot.add( - domain: (0, 10), - x => {calc.pow(10, x)}, - samples: 100, - line: "raw", - label: $y=10^x$ - ) - plot.add( - domain: (1, 10), - x => {x}, - samples: 100, - line: "raw", - hypograph: true, - label: $y=x$ - ) - } - ) -})) - -// Bode plot test -#box(stroke: 2pt + red,{ - canvas({ - import draw: * - cetz.draw.set-style( - grid: (stroke: (paint: luma(83.33%), thickness: 1pt, dash: "dotted")), - minor-grid: (stroke: (paint: luma(83.33%), thickness: 0.5pt, dash: "dotted")), - ) - plot.plot( - size: (16, 6), - axis-style: "scientific", - x-format: none, x-label: none, - x-mode: "log", - x-min: 0.01, x-max: 100, x-tick-step: 1, x-minor-tick-step: 1, - y-label: [Magnitude ($upright(d B)$)], - y-min: -40, y-max: 10, y-tick-step: 10, - x-grid: "both", - y-grid: "both", - { - plot.add(domain: (0.01, 100), x => {0}) - } - ) - }) - canvas({ - import draw: * - cetz.draw.set-style( - grid: (stroke: (paint: luma(83.33%), thickness: 1pt, dash: "dotted")), - minor-grid: (stroke: (paint: luma(83.33%), thickness: 0.5pt, dash: "dotted")), - ) - plot.plot( - size: (16, 6), - axis-style: "scientific", - x-mode: "log", - x-min: 0.01, x-max: 100, x-tick-step: 1, x-minor-tick-step: 1, - x-label: [Frequency ($upright(r a d)\/s$)], - y-label: [Phase ($upright(d e g)$)], - y-min: -90, y-max: 0, y-tick-step: 45, - x-grid: "both", - y-grid: "both", - { - plot.add(domain: (0.01, 100), x => {-40}) - } - ) - }) -}) - -// Column chart test -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot( - size: (9, 6), - axis-style: "scientific", - y-mode: "log", y-base: 10, - y-format: "sci", - x-min: -0.5, x-max: 4.5, x-tick-step: 1, - y-min: 0.1, y-max: 10000, y-tick-step: 1, y-minor-tick-step: 1, - x-grid: "both", - y-grid: "both", - { - plot.add-bar( - (1, 10, 100, 1000, 10000).enumerate().map(((x,y))=>{(x,y)}), - bar-width: 0.8, - ) - } - ) -})) - -// Scatter plot test -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot( - size: (9, 6), - axis-style: "scientific", - y-mode: "log", y-base: 100, - y-format: "sci", - x-min: -0.5, x-max: 4.5, x-tick-step: 1, - y-min: 0.1, y-max: 10000, y-tick-step: 1, y-minor-tick-step: 10, - x-grid: "both", - y-grid: "both", - { - plot.add( - ((0, 1),(1,2),(1,3),(2, 100),(2,150),(3, 1000),), - style: (stroke: none), - mark: "o" - ) - plot.annotate({ - rect((0, 1), (calc.pi, 10), fill: rgb(50,50,200,50)) - content((2, 3), [Annotation]) - }) - plot.annotate({ - rect((0, 1000), (calc.pi, 10000), fill: rgb(50,50,200,50)) - content((2, 3000), [Annotation]) - }) - } - ) -})) - -// Box plot test -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot( - size: (9, 6), - axis-style: "scientific", - y-mode: "log", y-base: 10, - y-format: "sci", - x-min: -0.5, x-max: 2.5, x-tick-step: 1, - y-min: 0.1, y-max: 15000, y-tick-step: 1, y-minor-tick-step: 1, - x-grid: "both", - y-grid: "both", - { - plot.add-boxwhisker( - ( - (x: 0, min: 1, q1: 10, q2: 100, q3: 1000, max: 10000), - (x: 1, min: 100, q1: 200, q2: 300, q3: 400, max: 500), - (x: 2, min: 10, q1: 100, q2: 500, q3: 1000, max: 5000), - ), - ) - } - ) -})) - diff --git a/tests/axes/ref/1.png b/tests/axes/ref/1.png deleted file mode 100644 index a7ed6a5..0000000 Binary files a/tests/axes/ref/1.png and /dev/null differ diff --git a/tests/axes/test.typ b/tests/axes/test.typ deleted file mode 100644 index 70bf522..0000000 --- a/tests/axes/test.typ +++ /dev/null @@ -1,69 +0,0 @@ -#set page(width: auto, height: auto) -#import "/tests/helper.typ": * -#import cetz: draw -#import cetz-plot: axes - -// 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", unit: [ units])), - 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: v => { - let d = v / calc.pi - if d == 0 {return $0$} - {$#{d}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))) -}) diff --git a/tests/chart/boxwhisker/ref/1.png b/tests/chart/boxwhisker/ref/1.png deleted file mode 100644 index 5486bae..0000000 Binary files a/tests/chart/boxwhisker/ref/1.png and /dev/null differ diff --git a/tests/chart/boxwhisker/test.typ b/tests/chart/boxwhisker/test.typ deleted file mode 100644 index 342f304..0000000 --- a/tests/chart/boxwhisker/test.typ +++ /dev/null @@ -1,26 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#let data0 = ( - ( - label: "Control", - min: 10,q1: 25,q2: 50, - q3: 75,max: 90 - ), - ( - label: "Condition aB", - min: 32,q1: 54,q2: 60, - q3: 69,max: 73, - outliers: (18, 23, 78,) - ), -) - -#test-case({ - chart.boxwhisker( - size: (10, 10), - y-min: 0, - y-max: 100, - label-key: "label", - data0) -}) diff --git a/tests/chart/piechart/ref/1.png b/tests/chart/piechart/ref/1.png deleted file mode 100644 index 79f0c4d..0000000 Binary files a/tests/chart/piechart/ref/1.png and /dev/null differ diff --git a/tests/chart/piechart/test.typ b/tests/chart/piechart/test.typ deleted file mode 100644 index 47fc19e..0000000 --- a/tests/chart/piechart/test.typ +++ /dev/null @@ -1,110 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/cetz.typ": * -#import "/src/lib.typ": * -#import chart: piechart -#import "/tests/helper.typ": * - -#let colors = gradient.linear(rgb("FFCCE5"), rgb("660033")) - -// Outset items -#test-case({ - import draw: * - piechart(range(1,11), outset: 3, outset-offset: 25%, slice-style: colors) -}) - -// Outset items + inner radius -#test-case({ - import draw: * - piechart(range(1,11), outset: 3, inner-radius: .5, outset-offset: 25%, slice-style: colors) -}) - -// Outset items + arc shape -#test-case({ - import draw: * - piechart(range(1,5), outset-offset: 25%, slice-style: colors, - start: 0deg, stop: 180deg) -}) - -// Outset items + inner radius -#test-case({ - import draw: * - piechart(range(1,5), inner-radius: .5, outset-offset: 25%, slice-style: colors, - start: 45deg, stop: 135deg) -}) - -// Rotated Values -#test-case({ - piechart(range(1,11), slice-style: colors, outer-label: (angle: auto, content: "VALUE")) -}) - -// Rotated Percentages -#test-case({ - piechart(range(10, 60, step: 10), slice-style: colors, outer-label: (angle: auto, content: "%")) -}) - -// Inner Values -#test-case({ - piechart(range(1,11), slice-style: colors, inner-label: (content: "VALUE"), radius: 2) -}) - -// Inner Percentages -#test-case({ - piechart(range(10, 60, step: 10), slice-style: colors, inner-label: (content: "%"), radius: 2) -}) - -// Gap as canvas size -#test-case({ - piechart(range(1,11), gap: .1, slice-style: colors) -}) - -// Gap as canvas size + inner radius -#test-case({ - piechart(range(1,11), gap: .1, inner-radius: .5, slice-style: colors) -}) - -// Gap as angle -#test-case({ - piechart(range(1,11), gap: 5deg, slice-style: colors, outer-label: (angle: auto)) -}) - -// Anchors -#test-case({ - import draw: * - piechart(range(1,11), slice-style: colors, name: "c", inner-radius: .5) - for-each-anchor("c", n => { - circle("c." + n, radius: .05) - }) -}) - -// Keys -#test-case({ - piechart(((value: 1, label: [One], o: false), - (value: 1, label: [Two], o: true)), slice-style: colors, - value-key: "value", label-key: "label", outer-label: (content: "LABEL", radius: 150%), outset-key: "o") -}) - -// Keys -#test-case({ - piechart(((value: 1, label: [One]), - (value: 1, label: [Two], o: 2%), - (value: 1, label: [Three], o: 4%), - (value: 1, label: [Four], o: 6%), - (value: 1, label: [Five], o: 8%), - (value: 1, label: [Six], o: 10%), - (value: 1, label: [Seven], o: 12%), - (value: 1, label: [Eight], o: 14%),), - slice-style: colors, - value-key: "value", label-key: "label", outer-label: (content: "LABEL", radius: 150%), outset-key: "o") -}) - -// Clockwise rotation -#test-case({ - import draw: * - piechart(range(1,4), clockwise: true, slice-style: (green, yellow, red)) -}) - -// Counter clockwise rotation -#test-case({ - import draw: * - piechart(range(1,4), clockwise: false, slice-style: (green, yellow, red)) -}) diff --git a/tests/chart/ref/1.png b/tests/chart/ref/1.png deleted file mode 100644 index 233c38c..0000000 Binary files a/tests/chart/ref/1.png and /dev/null differ diff --git a/tests/chart/test.typ b/tests/chart/test.typ deleted file mode 100644 index c0f8535..0000000 --- a/tests/chart/test.typ +++ /dev/null @@ -1,226 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/cetz.typ": * -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#let data0 = ( - ([1], 1), - ([2], 2), - ([3], 3), -) - -#let data1 = ( - ([15-24], 20.0), - ([25-29], 17.2), - ([30-34], 14.2), - ([35-44], 29.3), - ([45-54], 22.5), - ([55+], 18.4), -) - -#let data2 = ( - ([15-24], 18.0, 20.1, 23.0, 17.0), - ([25-29], 16.3, 17.6, 19.4, 15.3), - ([30-34], 14.0, 15.3, 13.9, 18.7), - ([35-44], 35.5, 26.5, 29.4, 25.8), - ([45-54], 25.0, 20.6, 22.4, 22.0), - ([55+], 19.9, 18.2, 19.2, 16.4), -) - -#let data3 = ( - (1, 0.001), - (2, 0.002), - (3, 0.003), -) - -#let data4 = ( - (1, 1, .3), - (2, 2, .2), - (3, 3, .1), -) - -#test-case({ - chart.barchart(mode: "basic", - size: (9, auto), - data0) -}) - -#test-case({ - chart.barchart(mode: "basic", - size: (9, auto), - value-key: 1, - label-key: 0, - x-tick-step: 5, - x-label: [x], - y-label: [y], - data1) -}) - -#test-case({ - chart.barchart(mode: "clustered", - size: (9, auto), - label-key: 0, - value-key: (..range(1, 5)), - data2) -}) - -#test-case({ - chart.barchart(mode: "stacked", - size: (9, auto), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -}) - -#test-case({ - chart.barchart(mode: "stacked100", - size: (9, auto), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -}) - -#test-case({ - chart.columnchart(mode: "basic", - size: (auto, 5), - data0) -}) - -#test-case({ - chart.columnchart(mode: "basic", - size: (auto, 5), - value-key: 1, - label-key: 0, - y-tick-step: 5, - x-label: [x], - y-label: [y], - data1) -}) - -#test-case({ - chart.columnchart(mode: "clustered", - size: (auto, 5), - label-key: 0, - value-key: (..range(1, 5)), - data2) -}) - -#test-case({ - chart.columnchart(mode: "stacked", - size: (auto, 5), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -}) - -#test-case({ - chart.columnchart(mode: "stacked100", - size: (auto, 4), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -}) - -#test-case({ - chart.columnchart( - size: (auto, 2), - y-tick-step: .5, - y-max: 1.0, - (([$ cal(P)_+ $], 4 / 13), ([$ cal(P)_- $], 9 / 13)) - ) - - draw.set-origin((4, 0)) - - chart.barchart( - size: (3, auto), - x-tick-step: .5, - x-max: 1.0, - (([$ cal(P)_+ $], 4 / 13), ([$ cal(P)_- $], 9 / 13)) - ) -}) - -#test-case({ - chart.columnchart( - size: (auto, 2), - y-tick-step: .5, - y-max: 1.0, - (([$ cal(P)_+ $], -4 / 13), ([$ cal(P)_- $], 9 / 13)) - ) - - draw.set-origin((4, 0)) - - chart.barchart( - size: (3, auto), - x-tick-step: .5, - x-max: 1.0, - (([$ cal(P)_+ $], 4 / 13), ([$ cal(P)_- $], -9 / 13)) - ) -}) - -#test-case({ - chart.columnchart( - size: (auto, 2), - y-tick-step: 0.001, - y-format: "sci", - data3) -}) - -#test-case({ - chart.columnchart( - size: (auto, 2), - y-tick-step: 0.001, - y-decimals: 3, - data3) -}) - -#test-case({ - chart.barchart( - size: (5, auto), - x-tick-step: 0.001, - x-format: "sci", - data3) -}) - -#test-case({ - chart.barchart( - size: (5, auto), - x-tick-step: 0.001, - x-decimals: 3, - data3) -}) - -#test-case({ - draw.set-style(barchart: (bar-width: 1, cluster-gap: .2)) - chart.barchart(mode: "clustered", - size: (5, auto), - label-key: 0, - value-key: (..range(1, 5)), - data2) -}) - -#test-case({ - draw.set-style(columnchart: (bar-width: 1, cluster-gap: .2)) - chart.columnchart(mode: "clustered", - size: (auto, 5), - label-key: 0, - value-key: (..range(1, 5)), - data2) -}) - -#test-case({ - chart.columnchart(mode: "basic", - size: (auto, 4), - error-key: 2, - data4) -}) - -#test-case({ - chart.barchart(mode: "basic", - size: (9, auto), - error-key: 2, - data4) -}) diff --git a/tests/elements/fill-between/ref/1.png b/tests/elements/fill-between/ref/1.png new file mode 100644 index 0000000..f96f555 Binary files /dev/null and b/tests/elements/fill-between/ref/1.png differ diff --git a/tests/elements/fill-between/test.typ b/tests/elements/fill-between/test.typ new file mode 100644 index 0000000..04c1da3 --- /dev/null +++ b/tests/elements/fill-between/test.typ @@ -0,0 +1,40 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + + // Sample function manually + let data = range(0,int(31)).map((t)=>{ + ( + 2 * calc.pi * t/30, // x + calc.pow(calc.sin(2 * calc.pi * t/30),2), // y + 0.1, // xerr + 0.02, // yerr + ) + }) + + cetz-plot.plot( + axis-style: cetz-plot.axis-style.orthorect-2d, + size: (12,7), + + x-tick-step: calc.pi / 4, + x-minor-tick-step: calc.pi / 16, + x-grid: "both", + x-min: 0, x-max: 2 * calc.pi, + x-format: cetz-plot.axes.format.multiple-of, + + y-tick-step: 0.5, y-minor-tick-step: 0.1, + y-grid: "both", + { + + cetz-plot.add.fill-between( + data.map(it=>(it.at(0), it.at(1)+it.at(2))), + data.map(it=>(it.at(0), it.at(1)-it.at(2))), + style: (stroke: none), + label: [95% C.I] + ) + + } + ) + +}) \ No newline at end of file diff --git a/tests/elements/series/ref/1.png b/tests/elements/series/ref/1.png new file mode 100644 index 0000000..56bc178 Binary files /dev/null and b/tests/elements/series/ref/1.png differ diff --git a/tests/elements/series/test.typ b/tests/elements/series/test.typ new file mode 100644 index 0000000..fb1956c --- /dev/null +++ b/tests/elements/series/test.typ @@ -0,0 +1,62 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + + // Sample function manually + let data = range(0,int(31)).map((t)=>{ + ( + 2 * calc.pi * t/30, // x + calc.pow(calc.sin(2 * calc.pi * t/30),2), // y + 0.1, // xerr + 0.02, // yerr + ) + }) + + cetz-plot.plot( + axis-style: cetz-plot.axis-style.orthorect-2d, + size: (12,7), + + x-tick-step: calc.pi / 4, + x-minor-tick-step: calc.pi / 16, + x-grid: "both", + x-min: 0, x-max: 2 * calc.pi, + x-format: cetz-plot.axes.format.multiple-of, + + y-tick-step: 0.5, y-minor-tick-step: 0.1, + y-grid: "both", + { + + cetz-plot.add.series( + label: [My Plot], + { + cetz-plot.add.xy( + data, + domain: (0, 2* calc.pi), + mark: "x", + line: "raw", + samples: 100, + label: $sin^2 (x)$ + ) + + cetz-plot.add.fill-between( + data.map(it=>(it.at(0), it.at(1)+it.at(2))), + data.map(it=>(it.at(0), it.at(1)-it.at(2))), + style: (stroke: none), + label: [95% C.I] + ) + + cetz-plot.add.errorbar( + data, + y-error-key: 2, + whisker-size: 0.1, + ) + } + ) + + + + } + ) + +}) \ No newline at end of file diff --git a/tests/plot/annotation/ref.png b/tests/plot/annotation/ref.png deleted file mode 100644 index 452e6f3..0000000 Binary files a/tests/plot/annotation/ref.png and /dev/null differ diff --git a/tests/plot/annotation/ref/1.png b/tests/plot/annotation/ref/1.png deleted file mode 100644 index 160481a..0000000 Binary files a/tests/plot/annotation/ref/1.png and /dev/null differ diff --git a/tests/plot/annotation/test.typ b/tests/plot/annotation/test.typ deleted file mode 100644 index 917066c..0000000 --- a/tests/plot/annotation/test.typ +++ /dev/null @@ -1,25 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#test-case({ - import draw: * - set-style(rect: (stroke: none)) - - plot.plot(size: (6, 4), { - plot.add(domain: (-calc.pi, 3*calc.pi), calc.sin) - plot.annotate(background: true, { - rect((0, -1), (calc.pi, 1), fill: blue.lighten(90%)) - rect((calc.pi, -1.1), (2*calc.pi, 1.1), fill: red.lighten(90%)) - rect((2*calc.pi, -1.5), (3.5*calc.pi, 1.5), fill: green.lighten(90%)) - }) - plot.annotate(padding: .1, { - line((calc.pi / 2, 1.1), (rel: (0, .2)), (rel: (2*calc.pi, 0)), (rel: (0, -.2))) - content((calc.pi * 1.5, 1.5), $ lambda $) - }) - plot.annotate(padding: .1, { - line((calc.pi / 2,-.1), (calc.pi / 2, .8), mark: (end: "stealth")) - }) - }) -}) diff --git a/tests/plot/bar/ref/1.png b/tests/plot/bar/ref/1.png deleted file mode 100644 index 8f18520..0000000 Binary files a/tests/plot/bar/ref/1.png and /dev/null differ diff --git a/tests/plot/bar/test.typ b/tests/plot/bar/test.typ deleted file mode 100644 index ca4ecae..0000000 --- a/tests/plot/bar/test.typ +++ /dev/null @@ -1,20 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/cetz.typ": * -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#let data = ( - (0, (1, 2, 3)), - (1, (6, 7, 8), (2, 1, 0)), - (2, 5, ()), -) - -#test-case({ - plot.plot(size: (3, 3), x-tick-step: 1, y-tick-step: 1, - { - plot.add-bar(data, - x-key: 0, - y-key: 1, - error-key: 2) - }) -}) diff --git a/tests/plot/boxwhisker/ref/1.png b/tests/plot/boxwhisker/ref/1.png deleted file mode 100644 index e3c1081..0000000 Binary files a/tests/plot/boxwhisker/ref/1.png and /dev/null differ diff --git a/tests/plot/boxwhisker/test.typ b/tests/plot/boxwhisker/test.typ deleted file mode 100644 index 5f5cc63..0000000 --- a/tests/plot/boxwhisker/test.typ +++ /dev/null @@ -1,57 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#let box1 = ( - outliers: (7, 65, 69), - min: 15, - q1: 25, - q2: 35, - q3: 50, - max: 60) - -#let box2 = ( - min: -1, - q1: 0, - q2: 3, - q3: 6, - max: 8) - -#test-case({ - import draw: * - - plot.plot(size: (10, 10), - y-min: 0, - y-max: 100, - { - plot.add-boxwhisker((x: 1, ..box1)) - }) -}) - -#test-case({ - import draw: * - - plot.plot(size: (10, 10), - y-min: 0, y-max: 100, - { - plot.add-boxwhisker(( - (x: 1, ..box1), - (x: 2, ..box1), - (x: 3, ..box1), - (x: 4, ..box1), - )) - }) -}) - -// Test auto-sizing of the plot -#test-case({ - import draw: * - - plot.plot(size: (10, 10), { - plot.add-boxwhisker(( - (x: 1, ..box1), - (x: 2, ..box2), - )) - }) -}) diff --git a/tests/plot/broken-axes/ref/1.png b/tests/plot/broken-axes/ref/1.png deleted file mode 100644 index ead6a97..0000000 Binary files a/tests/plot/broken-axes/ref/1.png and /dev/null differ diff --git a/tests/plot/broken-axes/test.typ b/tests/plot/broken-axes/test.typ deleted file mode 100644 index 19f09b2..0000000 --- a/tests/plot/broken-axes/test.typ +++ /dev/null @@ -1,23 +0,0 @@ -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#let data = ((5,5), (10,10)) - -#test-case({ - plot.plot(size: (8,8), - x-break: true, - y-break: true, - { - plot.add(data) - }) -}) - -#test-case({ - plot.plot(size: (8,8), - axis-style: "school-book", - x-break: true, - y-break: true, - { - plot.add(data) - }) -}) diff --git a/tests/plot/contour/ref/1.png b/tests/plot/contour/ref/1.png deleted file mode 100644 index 9af5dbf..0000000 Binary files a/tests/plot/contour/ref/1.png and /dev/null differ diff --git a/tests/plot/contour/test.typ b/tests/plot/contour/test.typ deleted file mode 100644 index 9d6d4d2..0000000 --- a/tests/plot/contour/test.typ +++ /dev/null @@ -1,130 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#let peaks(x, y) = ( - 3 * calc.pow(1 - x, 2) * calc.exp(-(x*x) - calc.pow(y + 1, 2)) - - 10 * (x/5 - calc.pow(x, 3) - calc.pow(y, 5)) * - calc.exp(-(x * x) - (y * y)) - 1/3 * calc.exp(-calc.pow(x + 1, 2) - (y * y)) -) - -/* Simple contour */ -#test-case({ - import draw: * - - plot.plot(size: (8, 8), - x-tick-step: 5, - y-tick-step: 5, - { - plot.add-contour( - (x, y) => 2 - (x - 1) * (y - 1), - fill: true, - x-domain: (-10, 10), - y-domain: (-10, 11), - ) - - plot.add-contour( - (x, y) => 30 - (calc.pow(1 - x, 2) + calc.pow(1 - y, 2)), - fill: true, - x-domain: (-10, 10), - y-domain: (-10, 10), - ) - }) -}) - -/* Multi contour */ -#test-case({ - import draw: * - - plot.plot(size: (8, 8), - x-tick-step: 1, - y-tick-step: 1, - { - plot.add-contour( - peaks, - z: (0, 1, 2, 3, 4), - fill: true, - x-domain: (-2, 3), - y-domain: (-2, 3), - x-samples: 50, - y-samples: 50, - ) - }) -}) - -/* Multi contour */ -#test-case({ - import draw: * - - plot.plot(size: (8, 8), - x-tick-step: 1, - y-tick-step: 1, - { - let z(x, y) = { - (1 - x/2 + calc.pow(x,5) + calc.pow(y,3)) * calc.exp(-(x*x) - (y*y)) - } - plot.add-contour( - z, - z: (-.68, -.39, -.1, .1, .47, .76, 1.05), - fill: true, - x-domain: (-3, 3), - y-domain: (-3, 3), - x-samples: 50, - y-samples: 50, - ) - }) -}) - -/* Complex contour #270 */ -#test-case({ - plot.plot(size: (8, 8), { - // x >= 0 - plot.add-contour( - (x, y) => x, - z: 0, - y-samples: 2, - x-samples: 2, - x-domain: (0, 10), - y-domain: (-10, 10), - fill: true, - ) - - // y >= 0 - plot.add-contour( - (x, y) => y, - z: 0, - y-samples: 2, - x-samples: 2, - x-domain: (-10, 10), - y-domain: (0, 10), - fill: true, - ) - - // hyperbola - plot.add-contour( - (x, y) => (x - 1) * (y - 1), - x-domain: (-10, 10), - y-domain: (-10, 10), - fill: true, - z: 1, - ) - - // circle - plot.add-contour( - (x, y) => (calc.pow((x - 1), 2) + calc.pow((y - 1), 2)), - x-domain: (-10, 10), - y-domain: (-10, 10), - z: 9, - op: "<=", - fill: true, - ) - - // line - plot.add-contour( - (x, y) => x + 1 - y, - x-domain: (-10, 10), - y-domain: (-10, 10), - ) - }) -}) diff --git a/tests/plot/equal-axis/ref/1.png b/tests/plot/equal-axis/ref/1.png deleted file mode 100644 index 58672e5..0000000 Binary files a/tests/plot/equal-axis/ref/1.png and /dev/null differ diff --git a/tests/plot/equal-axis/test.typ b/tests/plot/equal-axis/test.typ deleted file mode 100644 index eae91c5..0000000 --- a/tests/plot/equal-axis/test.typ +++ /dev/null @@ -1,36 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#test-case({ - import draw: * - - plot.plot(size: (6,3), - x-tick-step: none, - y-tick-step: none, - x-equal: "y", - a-equal: "b", - b-horizontal: true, - { - 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")) - }) -}) - -#test-case({ - import draw: * - - plot.plot(size: (3,6), - x-tick-step: none, - y-tick-step: none, - x-equal: "y", - a-equal: "b", - b-horizontal: true, - { - 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/format/ref/1.png b/tests/plot/format/ref/1.png deleted file mode 100644 index b46e922..0000000 Binary files a/tests/plot/format/ref/1.png and /dev/null differ diff --git a/tests/plot/format/test.typ b/tests/plot/format/test.typ deleted file mode 100644 index 53abf75..0000000 --- a/tests/plot/format/test.typ +++ /dev/null @@ -1,39 +0,0 @@ -#set page(width: auto, height: auto) -#import "/tests/helper.typ": * -#import cetz: draw -#import cetz-plot: plot - -#let data = ((-calc.pi, -1), (+calc.pi, +1)) - -#test-case({ - plot.plot( - size: (8, 4), - x-min: -2 * calc.pi, - x-max: +2 * calc.pi, - x-tick-step: calc.pi/2, - x-format: plot.formats.multiple-of, { - plot.add(data) - }) -}) - -#test-case({ - plot.plot( - size: (8, 4), - x-min: -2, - x-max: +2, - x-tick-step: 1/3, - x-format: plot.formats.fraction, { - plot.add(data) - }) -}) - -#test-case({ - plot.plot( - size: (8, 4), - x-min: -2, - x-max: +2, - x-tick-step: 1/3, - x-format: plot.formats.fraction.with(denom: 33), { - plot.add(data) - }) -}) diff --git a/tests/plot/grid/ref/1.png b/tests/plot/grid/ref/1.png deleted file mode 100644 index a62f0e4..0000000 Binary files a/tests/plot/grid/ref/1.png and /dev/null differ diff --git a/tests/plot/grid/test.typ b/tests/plot/grid/test.typ deleted file mode 100644 index 5d37857..0000000 --- a/tests/plot/grid/test.typ +++ /dev/null @@ -1,74 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* X grid */ -#test-case({ - import draw: * - - plot.plot(size: (3, 3), - x-grid: true, - x-tick-step: .5, - y-tick-step: .5, - { - plot.add(((0,0), (1,1))) - }) -}) - -/* X grid */ -#test-case({ - import draw: * - - plot.plot(size: (3, 3), - x-grid: "both", - x-tick-step: .5, - x-minor-tick-step: .25, - y-tick-step: .5, - { - plot.add(((0,0), (1,1))) - }) -}) - -/* Y grid */ -#test-case({ - import draw: * - - plot.plot(size: (3, 3), - y-grid: true, - x-tick-step: .5, - y-tick-step: .5, - { - plot.add(((0,0), (1,1))) - }) -}) - -/* Y grid */ -#test-case({ - import draw: * - - plot.plot(size: (3, 3), - y-grid: "both", - x-tick-step: .5, - y-tick-step: .5, - y-minor-tick-step: .25, - { - plot.add(((0,0), (1,1))) - }) -}) - -/* X-Y grid */ -#test-case({ - import draw: * - - plot.plot(size: (3, 3), - x-grid: "both", - y-grid: "both", - x-tick-step: .5, - x-minor-tick-step: .25, - y-tick-step: .5, - y-minor-tick-step: .25, - { - plot.add(((0,0), (1,1))) - }) -}) diff --git a/tests/plot/hvline/ref/1.png b/tests/plot/hvline/ref/1.png deleted file mode 100644 index b212394..0000000 Binary files a/tests/plot/hvline/ref/1.png and /dev/null differ diff --git a/tests/plot/hvline/test.typ b/tests/plot/hvline/test.typ deleted file mode 100644 index efc907f..0000000 --- a/tests/plot/hvline/test.typ +++ /dev/null @@ -1,61 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* Empty plot */ -#test-case({ - import draw: * - - plot.plot(size: (1, 1), - x-tick-step: none, - y-tick-step: none, - { - plot.add-vline(0) - plot.add-hline(0) - plot.add(((0,0), (1, 0))) - }) -}) - -/* Line plot + h/v line */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: none, - y-tick-step: none, - { - plot.add-vline(0) - plot.add-hline(0) - plot.add(((-1, -1), (1,1))) - }) -}) - -/* Line plot + Multiple h/v lines */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: none, - y-tick-step: none, - { - plot.add-vline(-.1, 0, .1) - plot.add-hline(-.1, 0, .1) - plot.add(((-2, -2), (2,2))) - }) -}) - -/* Clipped h/v lines */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: none, - y-tick-step: none, - x-min: 0, x-max: 2, - y-min: 0, y-max: 2, - { - plot.add-vline(-.1, 1, 3) - plot.add-hline(-.1, 1, 3) - }) -}) diff --git a/tests/plot/legend/ref/1.png b/tests/plot/legend/ref/1.png deleted file mode 100644 index cc29e58..0000000 Binary files a/tests/plot/legend/ref/1.png and /dev/null differ diff --git a/tests/plot/legend/test.typ b/tests/plot/legend/test.typ deleted file mode 100644 index 400b0af..0000000 --- a/tests/plot/legend/test.typ +++ /dev/null @@ -1,177 +0,0 @@ -#set page(width: auto, height: auto) -#import "/tests/helper.typ": * -#import cetz: draw -#import cetz-plot: plot - -#let dom = (domain: (0, 2 * calc.pi)) -#let fn(x, offset: 0) = {calc.sin(x) + offset} - -#for pos in ("north", "south", "west", "east", - "north-east", "north-west", - "south-east", "south-west",) { - test-case({ - import draw: * - - plot.plot(size: (2, 2), - x-tick-step: none, - y-tick-step: none, - legend: pos, - { - plot.add(..dom, fn, label: $ f(x) $) - }) - }) -} - -#for pos in ("inner-north", "inner-south", "inner-west", "inner-east", - "inner-north-east", "inner-north-west", - "inner-south-east", "inner-south-west",) { - test-case({ - import draw: * - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - legend: pos, - { - plot.add(..dom, fn, label: $ f(x) $) - }) - }) -} - -#test-case({ - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(..dom, fn, label: $ f_1(x) $) - plot.add(..dom, fn.with(offset: .1), label: $ f_2(x) $) - plot.add(..dom, fn.with(offset: .2), label: $ f_3(x) $) - }) -}) - -#test-case({ - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -}) - -#test-case({ - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(..dom, fn, fn.with(offset: .5), label: $ f(x) $) - }) -}) - -#test-case({ - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-hline(0, label: $ f(x) $) - plot.add-vline(0, label: $ f(x) $) - }) -}) - -#test-case({ - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-contour(x-domain: (-1, 1), y-domain: (-1, 1), - (x, y) => x, z: 0, op: "<=", label: $ f(x) $) - plot.add-contour(x-domain: (-1, 1), y-domain: (-1, 1), - (x, y) => x, z: 0, fill: true, label: $ f(x) $) - }) -}) - -#test-case({ - import draw: * - - let box1 = ( - x: 1, - outliers: (7, 65, 69), - min: 15, - q1: 25, - q2: 35, - q3: 50, - max: 60) - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-boxwhisker(box1, label: [Box]) - }) -}) - -#test-case({ - import draw: * - - set-style(legend: (item: (preview: (width: .4), spacing: .7), - orientation: ltr, default-position: "north")) - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -}) - -#test-case({ - import draw: * - - set-style(legend: (item: (preview: (width: .4, height: 1), spacing: 1), - padding: .1, - stroke: black, - fill: white, - orientation: ltr, default-position: "north")) - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -}) - -#test-case({ - plot.plot(size: (4, 2), - axis-style: "school-book", - legend-style: (offset: (-2.5, 1), - item: (preview: (margin: .5), spacing: .15), - fill: white, - stroke: (paint: black, dash: "dotted"), - padding: (.1, .5)), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -}) - -#test-case({ - plot.plot(size: (4,2), x-tick-step: none, y-tick-step: none, { - plot.add(domain: (0,1), x => x) - plot.add-legend([Custom 1]) - plot.add-legend([Custom 2], preview: () => { - import draw: * - set-style(stroke: blue) - line((0,0), (1,1)) - line((0,1), (1,0)) - }) - }) -}) diff --git a/tests/plot/line/between/ref/1.png b/tests/plot/line/between/ref/1.png deleted file mode 100644 index 75e8230..0000000 Binary files a/tests/plot/line/between/ref/1.png and /dev/null differ diff --git a/tests/plot/line/between/test.typ b/tests/plot/line/between/test.typ deleted file mode 100644 index 4f7839a..0000000 --- a/tests/plot/line/between/test.typ +++ /dev/null @@ -1,107 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#let size = (6, 4) -#let f(x, y: 0) = y + calc.sin(x * 1deg) - -/* Fill between */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -}) - -/* Fill between - Clip Top */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-max: .5, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -}) - -/* Fill between - Clip Bottom */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -.5, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -}) - -/* Fill between - Clip Top & Bottom */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-max: .5, - y-min: -.5, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -}) - -/* Fill between - Test 2 */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(domain: (0, 2 * calc.pi), - t => (calc.cos(t) * 1.5, calc.sin(t)), - t => (calc.cos(t), calc.sin(t) * 1.5)) - }) -}) - -/* Fill between - Test 3 */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(domain: (0, 2 * calc.pi), - t => (calc.cos(t) * 1.5, calc.sin(t) * 1.5), - t => (calc.cos(t), calc.sin(t))) - }) -}) - -/* Fill between - Test 4 */ -#test-case({ - import draw: * - - let f(x) = calc.sin(x) + calc.cos(3 * x) - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - // Function - plot.add(domain: (0, 4 * calc.pi), f) - // Error-Band fill - plot.add-fill-between(domain: (0, 4 * calc.pi), - style: (stroke: none), - x => f(x) - calc.exp(x/4) / 2, - x => f(x) + calc.exp(x/4) / 2) - }) -}) diff --git a/tests/plot/line/fill/ref/1.png b/tests/plot/line/fill/ref/1.png deleted file mode 100644 index 99f4d84..0000000 Binary files a/tests/plot/line/fill/ref/1.png and /dev/null differ diff --git a/tests/plot/line/fill/test.typ b/tests/plot/line/fill/test.typ deleted file mode 100644 index 1adad42..0000000 --- a/tests/plot/line/fill/test.typ +++ /dev/null @@ -1,178 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#let size = (6, 4) -#let f(x, y: 0) = y + calc.sin(x * 1deg) - -/* Epigraph/Hypograph */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add(domain: (-360, 360), epigraph: true, f) - plot.add(domain: (-360, 360), hypograph: true, f) - }) -}) - -/* Upper Half */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 0, - { - plot.add(domain: (-360, 360), epigraph: true, f) - plot.add(domain: (-360, 360), hypograph: true, f) - }) -}) - -/* Lower Half */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-max: 0, - { - plot.add(domain: (-360, 360), epigraph: true, f) - plot.add(domain: (-360, 360), hypograph: true, f) - }) -}) - -/* To Y=0 Clipped on Y<1 */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: -.5)) - }) -}) - -/* To Y=0 */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f) - }) -}) - -/* To Y=0 Clipped on Y>1 */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: +.5)) - }) -}) - -/* To Y=0 Offset +1.5 */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 0, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: +1.5)) - }) -}) - -/* To Y=0 Offset -1.5 */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 0, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: -1.5)) - }) -}) - -/* To Y=0 Out of range */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 1, y-max: 2, - { - plot.add(domain: (-360, 360), fill: true, f) - }) -}) - -/* Epigraph Full Fill */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 1, y-max: 2, - { - plot.add(domain: (-360, 360), epigraph: true, f) - }) -}) - -/* Hypograph Full Fill */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -2, y-max: -1, - { - plot.add(domain: (-360, 360), hypograph: true, f) - }) -}) - -/* Epigraph No Fill */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -2, y-max: -1, - { - plot.add(domain: (-360, 360), epigraph: true, f) - }) -}) - -/* Hypograph No Fill */ -#test-case({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 1, y-max: 2, - { - plot.add(domain: (-360, 360), hypograph: true, f) - }) -}) diff --git a/tests/plot/line/line-type/ref/1.png b/tests/plot/line/line-type/ref/1.png deleted file mode 100644 index e5ab591..0000000 Binary files a/tests/plot/line/line-type/ref/1.png and /dev/null differ diff --git a/tests/plot/line/line-type/test.typ b/tests/plot/line/line-type/test.typ deleted file mode 100644 index e6a57a4..0000000 --- a/tests/plot/line/line-type/test.typ +++ /dev/null @@ -1,25 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* Draw different line types */ -#test-case({ - import draw: * - - let data(i) = ((1, 2, 3, 4, 5).zip((1, 3, 2, 3, 1).map(v => v + i))) - plot.plot(size: (6, 6), - y-min: 0, y-max: 35, - x-tick-step: 1, - y-tick-step: 5, - { - plot.add(data(0), line: "linear", mark: "o") - plot.add(data(5), line: "spline", mark: "o") - plot.add(data(10), line: "hv", mark: "o") - plot.add(data(15), line: "vh", mark: "o") - plot.add(data(20), line: "hvh", mark: "o") - plot.add(data(25), line: (type: "hvh", mid: .25), mark: "o") - plot.add(data(30), line: (type: "hvh", mid: .75), mark: "o") - }) -}) - diff --git a/tests/plot/line/linearization/ref.png b/tests/plot/line/linearization/ref.png deleted file mode 100644 index c335f0a..0000000 Binary files a/tests/plot/line/linearization/ref.png and /dev/null differ diff --git a/tests/plot/line/linearization/ref/1.png b/tests/plot/line/linearization/ref/1.png deleted file mode 100644 index 2c589d2..0000000 Binary files a/tests/plot/line/linearization/ref/1.png and /dev/null differ diff --git a/tests/plot/line/linearization/test.typ b/tests/plot/line/linearization/test.typ deleted file mode 100644 index c8bdabb..0000000 --- a/tests/plot/line/linearization/test.typ +++ /dev/null @@ -1,28 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* Test linearization */ -#test-case({ - import draw: * - - plot.plot(size: (6, 4), - { - plot.add(domain: (0, 360), x=>calc.sin(x * 1deg), - line: "raw", style: (stroke: 3pt)) - plot.add(domain: (0, 360), x=>calc.sin(x * 1deg), - line: "linear") - }) -}) - -/* Test linearization for vertical and horizontal lines */ -#test-case({ - import draw: * - - plot.plot(size: (6, 4), - x-min: -1, x-max: 2, y-min: -1, y-max: 2, - { - plot.add(((0,0), (1,0), (1,0.1), (1,0.2), (1,0.5), (1,1), (0,1), (0,0))) - }) -}) diff --git a/tests/plot/line/mark/ref/1.png b/tests/plot/line/mark/ref/1.png deleted file mode 100644 index edf7a59..0000000 Binary files a/tests/plot/line/mark/ref/1.png and /dev/null differ diff --git a/tests/plot/line/mark/test.typ b/tests/plot/line/mark/test.typ deleted file mode 100644 index 2649e62..0000000 --- a/tests/plot/line/mark/test.typ +++ /dev/null @@ -1,28 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* Draw different marks */ -#test-case({ - import draw: * - - plot.plot(size: (5, 4), - axis-style: "scientific", - y-max: 2, - y-min: -2, - x-tick-step: 360, - y-tick-step: 1, - style: plot.palette.red, - mark-style: plot.palette.red, - { - for (i, m) in ("o", "square", "x", "triangle", "|", "-").enumerate() { - plot.add(domain: (i * 180, (i + 1) * 180), - samples: 12, - style: (stroke: none), - mark: m, - mark-size: .3, - x => calc.sin(x * 1deg)) - } - }) -}) diff --git a/tests/plot/line/spline/ref.png b/tests/plot/line/spline/ref.png deleted file mode 100644 index 939848e..0000000 Binary files a/tests/plot/line/spline/ref.png and /dev/null differ diff --git a/tests/plot/line/spline/ref/1.png b/tests/plot/line/spline/ref/1.png deleted file mode 100644 index 8533263..0000000 Binary files a/tests/plot/line/spline/ref/1.png and /dev/null differ diff --git a/tests/plot/line/spline/test.typ b/tests/plot/line/spline/test.typ deleted file mode 100644 index fe6634d..0000000 --- a/tests/plot/line/spline/test.typ +++ /dev/null @@ -1,16 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* Draw smoothed data by using spline interpolation */ -#test-case({ - plot.plot(size: (6, 4), - { - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: (type: "spline", tension: .40, - samples: 5)) - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: (type: "spline", tension: .47)) - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: "spline") - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: (type: "spline", tension: .5)) - }) -}) diff --git a/tests/plot/marks/ref/1.png b/tests/plot/marks/ref/1.png deleted file mode 100644 index 5df4f86..0000000 Binary files a/tests/plot/marks/ref/1.png and /dev/null differ diff --git a/tests/plot/marks/test.typ b/tests/plot/marks/test.typ deleted file mode 100644 index 7daba6c..0000000 --- a/tests/plot/marks/test.typ +++ /dev/null @@ -1,26 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/cetz.typ": * -#import "/src/lib.typ" as cetz-plot -#import "/tests/helper.typ": * - -// cetz-plot #13 -#test-case({ - import cetz-plot: plot - - let axis-options = (("x", "y"), ("x2", "y"), ("x", "y2"), ("x2", "y2")) - - plot.plot( - size: (5,5), - x-min: 0, x-max: 1, - y-min: 0, y-max: 1, - x2-min: 1, x2-max: 0, - y2-min: 1, y2-max: 0, - for axes in axis-options { - plot.add( - axes: axes, - mark: "o", - ((0.1,0.1), (0.4,0.4)) - ) - } - ) -}) diff --git a/tests/plot/mirror-axes/ref/1.png b/tests/plot/mirror-axes/ref/1.png deleted file mode 100644 index 2b86b6e..0000000 Binary files a/tests/plot/mirror-axes/ref/1.png and /dev/null differ diff --git a/tests/plot/mirror-axes/test.typ b/tests/plot/mirror-axes/test.typ deleted file mode 100644 index c7d9dc8..0000000 --- a/tests/plot/mirror-axes/test.typ +++ /dev/null @@ -1,12 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#test-case({ - // Force showing tick labels for mirrored axes - cetz.draw.set-style(axes: (tick: (label: ("show": true)))) - - cetz.plot.plot(size: (8,8), { - cetz.plot.add(domain: (0, 1), x => x) - }) -}) diff --git a/tests/plot/parametric/ref/1.png b/tests/plot/parametric/ref/1.png deleted file mode 100644 index b1924fd..0000000 Binary files a/tests/plot/parametric/ref/1.png and /dev/null differ diff --git a/tests/plot/parametric/test.typ b/tests/plot/parametric/test.typ deleted file mode 100644 index 3e58377..0000000 --- a/tests/plot/parametric/test.typ +++ /dev/null @@ -1,94 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* Simple plot */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi)) - }) -}) - -/* Test clipping */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad) + .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi)) - plot.add((t) => (calc.cos(t * 1rad) - .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi)) - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) + .5), - domain: (0, 2 * calc.pi)) - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) - .5), - domain: (0, 2 * calc.pi)) - }) -}) - -/* Test filling */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi), - fill: true) - }) -}) - -/* Test clipping + filling */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad) + .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - plot.add((t) => (calc.cos(t * 1rad) - .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) + .5), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) - .5), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - }) -}) - -/* Test clipping + filling */ -#test-case({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - y-max: .5, y-min: -.5, - x-max: 1, x-min: -1, - { - let f(t, off: 0) = {(calc.cos(t) / (calc.pow(calc.sin(t), 2) + 1) + off, - calc.cos(t) * calc.sin(t) / (calc.pow(calc.sin(t), 2) + 1) + off)} - plot.add(samples: 50, - domain: (0, 2 * calc.pi), f, fill:true, fill-type: "shape") - plot.add(samples: 50, - domain: (0, 2 * calc.pi), f.with(off: .4), fill:true, fill-type: "shape") - plot.add(samples: 50, - domain: (0, 2 * calc.pi), f.with(off: -.4), fill:true, fill-type: "shape") - }) -}) diff --git a/tests/plot/ref.png b/tests/plot/ref.png deleted file mode 100644 index d52620e..0000000 Binary files a/tests/plot/ref.png and /dev/null differ diff --git a/tests/plot/ref/1.png b/tests/plot/ref/1.png deleted file mode 100644 index f3e3a1c..0000000 Binary files a/tests/plot/ref/1.png and /dev/null differ diff --git a/tests/plot/reverse-axis/ref.png b/tests/plot/reverse-axis/ref.png deleted file mode 100644 index 0c5896d..0000000 Binary files a/tests/plot/reverse-axis/ref.png and /dev/null differ diff --git a/tests/plot/reverse-axis/ref/1.png b/tests/plot/reverse-axis/ref/1.png deleted file mode 100644 index a2d87cc..0000000 Binary files a/tests/plot/reverse-axis/ref/1.png and /dev/null differ diff --git a/tests/plot/reverse-axis/test.typ b/tests/plot/reverse-axis/test.typ deleted file mode 100644 index 726deff..0000000 --- a/tests/plot/reverse-axis/test.typ +++ /dev/null @@ -1,18 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#test-case({ - plot.plot(size: (10, 10), x-min: 9, x-max: 0, - { - plot.add(domain: (0, 9), calc.sqrt) - }) -}) - -#test-case({ - plot.plot(size: (10, 10), y-min: 9, y-max: 0, - { - plot.add(domain: (-5, 5), x => calc.pow(x, 2)) - }) -}) diff --git a/tests/plot/sample/sample.typ b/tests/plot/sample/sample.typ deleted file mode 100644 index 54d617e..0000000 --- a/tests/plot/sample/sample.typ +++ /dev/null @@ -1,37 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let cases = ( - (samples: 2, res: ((0,0), (100,10))), - (samples: 5, res: ((0,0), (25,2.5), (50,5.0), (75,7.5), (100,10.0))), - (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, - sample-at: c.at("extra", default: ())) - assert.eq(pts, c.res, - message: "Expected: " + repr(c.res) + ", got: " + repr(pts)) -} - -#let cases = ( - (samples: (2,2), res: (( 0,100), - (100,200))), - (samples: (3,3), res: (( 0, 50,100), - ( 50,100,150), - (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)) - assert.eq(rows, c.res, - message: "Expected: " + repr(c.res) + ", got: " + repr(rows)) -} - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3, 1), axis-style: none, { - plot.add(domain: (0, 100), x => 0, mark: "x", samples: 2) - plot.add(domain: (0, 100), x => 1, mark: "x", samples: 5) - }) -})) diff --git a/tests/plot/test.typ b/tests/plot/test.typ deleted file mode 100644 index 9ea7b68..0000000 --- a/tests/plot/test.typ +++ /dev/null @@ -1,288 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/cetz.typ": * -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#let line-data = ((-1,-1), (1,1),) - -#let data = (..(for x in range(-360, 360 + 1) { - ((x, calc.sin(x * 1deg)),) -})) - -/* Scientific Style */ -#test-case({ - plot.plot(size: (5, 2), - x-tick-step: 180, - y-tick-step: 1, - x-grid: "major", - y-grid: "major", - { - plot.add(data) - }) -}) - -/* 4-Axes */ -#test-case({ - plot.plot(size: (5, 3), - x-tick-step: 180, - 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, - { - plot.add(data) - plot.add(data, style: (stroke: blue), axes: ("x2", "y2")) - }) -}) - -/* School-Book Style */ -#test-case({ - plot.plot(size: (5, 4), - axis-style: "school-book", - x-tick-step: 180, - y-tick-step: 1, - { - plot.add(data) - }) -}) - -/* Clipping */ -#test-case({ - plot.plot(size: (5, 4), - axis-style: "school-book", - x-min: auto, - x-max: 350, - x-tick-step: 180, - y-min: -.5, - y-max: .5, - y-tick-step: 1, - { - plot.add(data) - }) -}) - -/* Palettes */ -#test-case({ - 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, - { - for i in range(0, 7) { - plot.add(domain: (i * 180, (i + 1) * 180), - epigraph: true, - style: plot.palette.rainbow, - x => calc.sin(x * 1deg)) - } - }) -}) - -/* Tick Step Calculation */ -#test-case({ - plot.plot(size: (12, 4), - y2-decimals: 4, - { - plot.add(((0,0), (1,10)), axes: ("x", "y")) - plot.add(((0,0), (.1,.01)), axes: ("x2", "y2")) - }) -}) - -#test-case({ - plot.plot(size: (12, 4), - y2-decimals: 9, - x2-decimals: 9, - y2-format: "sci", - { - plot.add(((0,0), (30,2500)), axes: ("x", "y")) - plot.add(((0,0), (.001,.0001)), axes: ("x2", "y2")) - }) -}) - -/* Axis Styles */ - - -#test-case(args => { - plot.plot(size: (4,4), x-tick-step: 90, y-tick-step: 1, - axis-style: args, { - plot.add(domain: (0, 360), x => calc.sin(x * 1deg)) - }) -}, args: ( - "scientific", "scientific-auto", "left", "school-book", none -)) - -/* Manual Axis Bounds */ -#let circle-data = range(0, 361).map( - t => (.5 * calc.cos(t*1deg), .5 * calc.sin(t*1deg))) -#test-case({ - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - xl-min: -1.5, xl-max: .5, - xr-min: -.5, xr-max: 1.5, - yb-min: -1.5, yb-max: .5, - yt-min: -.5, yt-max: 1.5, - { - plot.add(circle-data) - plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) - plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) - plot.add(circle-data, axes: ("x", "yt"), style: (stroke: blue)) - plot.add(circle-data, axes: ("x", "yb"), style: (stroke: yellow)) - }) -}) - -#test-case({ - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - xl-min: -1.75, xl-max: .25, - xr-min: -.25, xr-max: 1.75, - yb-min: -1.75, yb-max: .25, - yt-min: -.25, yt-max: 1.75, - { - plot.add(circle-data) - plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) - plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) - plot.add(circle-data, axes: ("x", "yt"), style: (stroke: blue)) - plot.add(circle-data, axes: ("x", "yb"), style: (stroke: yellow)) - }) -}),)) - -/* Anchors */ -#test-case({ - import draw: * - - plot.plot(size: (5, 3), name: "plot", - x-tick-step: 180, - y-tick-step: 1, - x-grid: "major", - y-grid: "major", - { - plot.add(data, fill: true) - plot.add-anchor("from", (-270, "max")) - plot.add-anchor("to", (90, "max")) - plot.add-anchor("lo", (90, 0)) - plot.add-anchor("hi", (90, "max")) - }) - - line((rel: (0, .2), to: "plot.from"), - (rel: (0, .2), to: "plot.to"), - mark: (start: "|", end: "|"), name: "annotation") - content((rel: (0, .1), to: ("annotation.start", 50%, "annotation.end")), $2 pi$, anchor: "south") - - line((rel: (0, .2), to: "plot.lo"), - (rel: (0, -.2), to: "plot.hi"), - 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), - x-tick-step: none, - x-ticks: (-1, 0, 1), - x-format: x => $x_(#x)$, - 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)$, - { - plot.add(samples: 2, domain: (-1, 1), x => -x, axes: ("x", "y")) - plot.add(samples: 2, domain: (-1, 1), x => x, axes: ("x2", "y2")) - }) -}) - -// Test plot with anchors only -#test-case({ - import draw: * - - plot.plot(size: (6, 4), name: "plot", - x-min: -1, x-max: 1, y-min: -1, y-max: 1, - { - plot.add-anchor("test", (0,0)) - }) - - circle("plot.test", radius: 1) -}) - -// Test empty plot -#test-case({ - plot.plot(size: (1, 1), {}) -}) - -// Some axis styling -#test-case({ - import draw: * - - set-style(axes: ( - padding: .1, - tick: ( - length: -.1, - ), - left: ( - stroke: (paint: red), - tick: ( - stroke: auto, - ) - ), - bottom: ( - stroke: (paint: blue, thickness: 2pt), - tick: ( - stroke: auto, - ) - ), - )) - - plot.plot(size: (6, 4), axis-style: "scientific-auto", { - plot.add(line-data) - }) - - set-origin((7, 0)) - - set-style(axes: ( - overshoot: .5, - x: ( - padding: 1, - overshoot: -.5, - stroke: blue, - ), - y: ( - stroke: red, - ) - )) - plot.plot(size: (6, 4), axis-style: "school-book", - x-tick-step: none, - y-tick-step: none, - { - plot.add(line-data) - }) -}) diff --git a/tests/plot/vertical/ref/1.png b/tests/plot/vertical/ref/1.png deleted file mode 100644 index 6cbfb6f..0000000 Binary files a/tests/plot/vertical/ref/1.png and /dev/null differ diff --git a/tests/plot/vertical/test.typ b/tests/plot/vertical/test.typ deleted file mode 100644 index 98d9420..0000000 --- a/tests/plot/vertical/test.typ +++ /dev/null @@ -1,51 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -#test-case({ - import draw: * - - plot.plot(size: (10, 10), - { - plot.add(domain: (0, 4*calc.pi), calc.sin, axes: ("y", "x")) - }) -}) - -#test-case({ - import draw: * - - plot.plot(size: (10, 10), - { - plot.add-contour(x-domain: (0, 4), y-domain: (-2, 2), - (x, y) => x - .5 * y, op: ">=", z: 2, axes: ("y", "x"), fill: true) - }) -}) - -#test-case({ - import draw: * - - let box1 = ( - outliers: (7, 65, 69), - min: 15, - q1: 25, - q2: 35, - q3: 50, - max: 60) - - plot.plot(size: (10, 10), - { - plot.add-boxwhisker((x: 1, ..box1), axes: ("y", "x")) - }) -}) - -#test-case({ - import draw: * - - plot.plot(size: (10, 10), y-label: $ x $, - x-label: $ y $, - x-min: -.75, x-max: .75, - { - plot.add(domain: (0, 4*calc.pi), calc.sin, axes: ("y", "x")) - }) -}) diff --git a/tests/plot/violin/ref/1.png b/tests/plot/violin/ref/1.png deleted file mode 100644 index 88e6d7d..0000000 Binary files a/tests/plot/violin/ref/1.png and /dev/null differ diff --git a/tests/plot/violin/test.typ b/tests/plot/violin/test.typ deleted file mode 100644 index ed1cbfc..0000000 --- a/tests/plot/violin/test.typ +++ /dev/null @@ -1,63 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/src/cetz.typ": * -#import "/tests/helper.typ": * - -/* Empty plot */ -#test-case({ - import draw: * - - draw.set-style( - axes: ( - stroke: 0.55pt, - tick: ( - stroke: 0.5pt, - ) - ), - legend: ( - stroke: none, - ) - ) - - let default-colors = (palette.blue-colors.at(3), palette.pink-colors.at(3)) - - plot.plot(size: (9, 6), - - y-label: [Age], - y-min: -10, y-max: 20, - y-tick-step: 10, y-minor-tick-step: 5, - y-grid: "major", - - x-label: [Class], - x-min: -0.5, x-max: 2.5, - x-tick-step: none, - x-ticks: ( (0, [First]), (1, [Second]), (2, [Third])), - - plot-style: (i) => { - let color = default-colors.at(calc.rem(i, default-colors.len())) - (stroke: color + 0.75pt, fill: color.lighten(75%)) - }, - { - let vals = ( - (0,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)), - (1,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)), - (2,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)), - ) - - cetz-plot.plot.add-violin( - vals, - extents: 0.5, - side: "left", - bandwidth: 0.45, - label: [Male], - ) - - cetz-plot.plot.add-violin( - vals, - extents: 0.5, - side: "right", - bandwidth: 0.5, - label: [Female] - ) - }) -}) \ No newline at end of file diff --git a/tests/plots/orthorect-2d/annotation/ref/1.png b/tests/plots/orthorect-2d/annotation/ref/1.png new file mode 100644 index 0000000..122a2ad Binary files /dev/null and b/tests/plots/orthorect-2d/annotation/ref/1.png differ diff --git a/tests/plots/orthorect-2d/annotation/test.typ b/tests/plots/orthorect-2d/annotation/test.typ new file mode 100644 index 0000000..e471938 --- /dev/null +++ b/tests/plots/orthorect-2d/annotation/test.typ @@ -0,0 +1,22 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + cetz.draw.set-style(rect: (stroke: none)) + + cetz-plot.plot(size: (6, 4), { + cetz-plot.add.xy(domain: (-calc.pi, 3*calc.pi), calc.sin, samples: 100) + cetz-plot.add.annotation(background: true, { + cetz.draw.rect((0, -1), (calc.pi, 1), fill: blue.lighten(90%)) + cetz.draw.rect((calc.pi, -1.1), (2*calc.pi, 1.1), fill: red.lighten(90%)) + cetz.draw.rect((2*calc.pi, -1.5), (3.5*calc.pi, 1.5), fill: green.lighten(90%)) + }) + cetz-plot.add.annotation(padding: .1, { + cetz.draw.line((calc.pi / 2, 1.1), (rel: (0, .2)), (rel: (2*calc.pi, 0)), (rel: (0, -.2))) + cetz.draw.content((calc.pi * 1.5, 1.5), $ lambda $) + }) + cetz-plot.add.annotation(padding: .1, { + cetz.draw.line((calc.pi / 2,-.1), (calc.pi / 2, .8), mark: (end: "stealth")) + }) + }) +}) \ No newline at end of file diff --git a/tests/plots/orthorect-2d/errorbar/ref/1.png b/tests/plots/orthorect-2d/errorbar/ref/1.png new file mode 100644 index 0000000..a8ba61d Binary files /dev/null and b/tests/plots/orthorect-2d/errorbar/ref/1.png differ diff --git a/tests/plots/orthorect-2d/errorbar/test.typ b/tests/plots/orthorect-2d/errorbar/test.typ new file mode 100644 index 0000000..f4f6a29 --- /dev/null +++ b/tests/plots/orthorect-2d/errorbar/test.typ @@ -0,0 +1,48 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + + // Sample function manually + let data = range(0,int(16)).map((t)=>{ + ( + 2 * calc.pi * t/15, // x + calc.pow(calc.sin(2 * calc.pi * t/15),2), // y + 0.1, // xerr + 0.02, // yerr + ) + }) + + cetz-plot.plot( + axis-style: cetz-plot.axis-style.orthorect-2d, + size: (12,7), + + x-tick-step: calc.pi / 4, + x-minor-tick-step: calc.pi / 16, + x-grid: "both", + x-min: 0, x-max: 2 * calc.pi, + x-format: cetz-plot.axes.format.multiple-of, + + y-tick-step: 0.5, y-minor-tick-step: 0.1, + y-grid: "both", + { + + cetz-plot.add.xy( + data, + domain: (0, 2* calc.pi), + line: "raw", + samples: 100, + label: $sin^2 (x)$ + ) + + cetz-plot.add.errorbar( + data, + y-error-key: 2, + x-error-key: 3, + whisker-size: 0.1, + ) + + } + ) + +}) \ No newline at end of file diff --git a/tests/plots/orthorect-2d/logarithmic-axes/ref/1.png b/tests/plots/orthorect-2d/logarithmic-axes/ref/1.png new file mode 100644 index 0000000..20b22ac Binary files /dev/null and b/tests/plots/orthorect-2d/logarithmic-axes/ref/1.png differ diff --git a/tests/plots/orthorect-2d/logarithmic-axes/test.typ b/tests/plots/orthorect-2d/logarithmic-axes/test.typ new file mode 100644 index 0000000..0176c7d --- /dev/null +++ b/tests/plots/orthorect-2d/logarithmic-axes/test.typ @@ -0,0 +1,33 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + + cetz-plot.plot( + size: (9, 6), + 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", + { + cetz-plot.add.xy( + domain: (0, 10), + x => {calc.pow(10, x)}, + samples: 100, + line: "raw", + label: $y=10^x$ + ) + cetz-plot.add.xy( + domain: (1, 10), + x => {x}, + samples: 100, + line: "raw", + hypograph: true, + label: $y=x$ + ) + } + ) + +}) \ No newline at end of file diff --git a/tests/plots/orthorect-2d/scatter/ref/1.png b/tests/plots/orthorect-2d/scatter/ref/1.png new file mode 100644 index 0000000..de5c556 Binary files /dev/null and b/tests/plots/orthorect-2d/scatter/ref/1.png differ diff --git a/tests/plots/orthorect-2d/scatter/test.typ b/tests/plots/orthorect-2d/scatter/test.typ new file mode 100644 index 0000000..51cc5f4 --- /dev/null +++ b/tests/plots/orthorect-2d/scatter/test.typ @@ -0,0 +1,38 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + // cetz.draw.set-style(axes:( fill: luma(85%))) + cetz-plot.plot( + axis-style: cetz-plot.axis-style.orthorect-2d, + size: (12,7), + + x-tick-step: calc.pi / 4, + x-minor-tick-step: calc.pi / 16, + x-grid: "both", + x-min: 0, x-max: 2 * calc.pi, + x-format: cetz-plot.axes.format.multiple-of, + + y-min: -1, y-max: 1, y-tick-step: 0.5, y-minor-tick-step: 0.1, + y-grid: "both", + { + cetz-plot.add.xy( + calc.sin, + domain: (0,2*calc.pi), + label: $y=x$, + line: "raw", + samples: 100, + epigraph: true, + ) + + cetz-plot.add.xy( + (t)=>calc.pow(calc.sin(t),2), + domain: (0, 2* calc.pi), + line: "raw", + samples: 100, + hypograph: true, + label: $sin^2 (x)$ + ) + } + ) +}) \ No newline at end of file diff --git a/tests/plots/orthorect-2d/violin/ref/1.png b/tests/plots/orthorect-2d/violin/ref/1.png new file mode 100644 index 0000000..7212907 Binary files /dev/null and b/tests/plots/orthorect-2d/violin/ref/1.png differ diff --git a/tests/plots/orthorect-2d/violin/test.typ b/tests/plots/orthorect-2d/violin/test.typ new file mode 100644 index 0000000..bd33040 --- /dev/null +++ b/tests/plots/orthorect-2d/violin/test.typ @@ -0,0 +1,43 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + + let vals = ( + (0,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)), + (1,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)), + (2,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)), + ) + + cetz-plot.plot( + size: (9, 6), + + y-label: [Age], + y-min: -10, y-max: 20, + y-tick-step: 10, y-minor-tick-step: 5, + y-grid: "major", + + x-label: [Class], + x-min: -0.5, x-max: 2.5, + x-tick-step: none, + x-ticks: ( (0, [First]), (1, [Second]), (2, [Third])), + { + cetz-plot.add.violin( + vals, + extents: 0.5, + side: "left", + bandwidth: 0.45, + label: [Male], + ) + + cetz-plot.add.violin( + vals, + extents: 0.5, + side: "right", + bandwidth: 0.5, + label: [Female] + ) + } + ) + +}) \ No newline at end of file diff --git a/tests/plots/polar-2d/scatter/ref/1.png b/tests/plots/polar-2d/scatter/ref/1.png new file mode 100644 index 0000000..4234cfb Binary files /dev/null and b/tests/plots/polar-2d/scatter/ref/1.png differ diff --git a/tests/plots/polar-2d/scatter/test.typ b/tests/plots/polar-2d/scatter/test.typ new file mode 100644 index 0000000..3c86cee --- /dev/null +++ b/tests/plots/polar-2d/scatter/test.typ @@ -0,0 +1,38 @@ +#set page(width: auto, height: auto, margin: 1cm) +#import "/tests/helper.typ": * + +#test-case({ + cetz.draw.set-style(axes:( fill: luma(91.37%).transparentize(90%))) + cetz-plot.plot( + axis-style: cetz-plot.axis-style.polar-2d, + size: (16,9), + + x-tick-step: calc.pi / 4, + x-minor-tick-step: calc.pi / 16, + x-grid: "both", + x-min: 0, x-max: 2 * calc.pi, + x-format: cetz-plot.axes.format.multiple-of, + + y-min: -1, y-max: 1, y-tick-step: 0.5, y-minor-tick-step: 0.1, + y-grid: "both", + { + cetz-plot.add.xy( + calc.sin, + domain: (0,2*calc.pi), + label: $y=x$, + line: "raw", + samples: 100, + epigraph: true, + ) + + cetz-plot.add.xy( + (t)=>calc.pow(calc.sin(t),2), + domain: (0, 2* calc.pi), + line: "raw", + samples: 100, + hypograph: true, + label: $sin^2 (x)$ + ) + } + ) +}) \ No newline at end of file