diff --git a/packages/preview/cetz-plot/0.1.4/LICENSE b/packages/preview/cetz-plot/0.1.4/LICENSE
new file mode 100644
index 0000000000..65c5ca88a6
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/LICENSE
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/packages/preview/cetz-plot/0.1.4/README.md b/packages/preview/cetz-plot/0.1.4/README.md
new file mode 100644
index 0000000000..3f36cda74d
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/README.md
@@ -0,0 +1,89 @@
+# CeTZ-Plot
+
+CeTZ-Plot is a library that adds plots and charts to [CeTZ](https://github.com/cetz-package/cetz), a library for drawing with [Typst](https://typst.app).
+
+CeTZ-Plot requires CeTZ version ≥ 0.5.0!
+
+## Examples
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+ | Plot |
+ Pie Chart |
+ Clustered Barchart |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+ | Pyramid |
+ Process |
+
+
+
+
+
+
+ |
+
+ | Cycle |
+
+
+
+*Click on the example image to jump to the code.*
+
+
+## Usage
+
+For information, see the [manual ](./manual.pdf?raw=true).
+
+To use this package, simply add the following code to your document:
+```
+#import "@preview/cetz:0.5.2"
+#import "@preview/cetz-plot:0.1.4": plot, chart
+
+#cetz.canvas({
+ // Your plot/chart code goes here
+})
+```
+
+## Installing
+
+To install the CeTZ-Plot package under [your local typst package dir](https://github.com/typst/packages?tab=readme-ov-file#local-packages) you can use the `install` script from the repository.
+
+### Just
+
+This project uses [just](https://github.com/casey/just), a handy command runner.
+
+You can run all commands without having `just` installed, just have a look into the `justfile`.
+To install `just` on your system, use your systems package manager. On Windows, [Cargo](https://doc.rust-lang.org/cargo/) (`cargo install just`), [Chocolatey](https://chocolatey.org/) (`choco install just`) and [some other sources](https://just.systems/man/en/chapter_4.html) can be used. You need to run it from a `sh` compatible shell on Windows (e.g git-bash).
+
+## Testing
+
+This package comes with some unit tests under the `tests` directory.
+To run all tests you can run the `just test` target. You need to have
+[`tytanic`](https://github.com/tingerrr/tytanic/) in your `PATH`: `cargo install tytanic --git https://github.com/tingerrr/tytanic`.
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/.gitkeep b/packages/preview/cetz-plot/0.1.4/gallery/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/barchart.png b/packages/preview/cetz-plot/0.1.4/gallery/barchart.png
new file mode 100644
index 0000000000..4a412e2a65
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/barchart.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/barchart.typ b/packages/preview/cetz-plot/0.1.4/gallery/barchart.typ
new file mode 100644
index 0000000000..53693501c7
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/barchart.typ
@@ -0,0 +1,25 @@
+#import "@preview/cetz:0.5.2": canvas, draw
+#import "@preview/cetz-plot:0.1.4": chart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#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),
+)
+
+#canvas({
+ draw.set-style(legend: (fill: white), barchart: (bar-width: .8, cluster-gap: 0))
+ chart.barchart(mode: "clustered",
+ size: (9, auto),
+ label-key: 0,
+ value-key: (..range(1, 5)),
+ x-tick-step: 2.5,
+ data2,
+ labels: ([Low], [Medium], [High], [Very high]),
+ legend: "inner-north-east",)
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/bending.png b/packages/preview/cetz-plot/0.1.4/gallery/bending.png
new file mode 100644
index 0000000000..a9c49832b7
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/bending.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/bending.typ b/packages/preview/cetz-plot/0.1.4/gallery/bending.typ
new file mode 100644
index 0000000000..312b4a82c8
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/bending.typ
@@ -0,0 +1,59 @@
+#import "@preview/cetz:0.5.2" as cetz: draw
+#import "/src/lib.typ": smartart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let steps = (
+ [Step 1],
+ [Step 2],
+ [Step 3],
+ [Step 4],
+ [Step 5],
+ [Step 6],
+)
+
+#let colors = (
+ red, orange, yellow.mix(green), green, green.mix(blue), blue
+).map(c => c.lighten(40%))
+
+#cetz.canvas({
+ smartart.process.bending(
+ steps,
+ step-style: colors,
+ equal-width: true,
+ name: "chart",
+ layout: (
+ flow: (ltr, btt),
+ max-stride: 2
+ )
+ )
+})
+
+/*
+#let flows = (
+ (ltr, ttb),
+ (ltr, btt),
+ (rtl, ttb),
+ (rtl, btt),
+ (ttb, ltr),
+ (btt, ltr),
+ (ttb, rtl),
+ (btt, rtl),
+)
+
+#for flow in flows {
+ cetz.canvas({
+ smartart.process.bending(
+ steps,
+ step-style: colors,
+ equal-width: true,
+ name: "chart",
+ layout: (
+ flow: flow,
+ max-stride: 3
+ )
+ )
+ })
+ pagebreak(weak: true)
+}
+*/
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/chevron.png b/packages/preview/cetz-plot/0.1.4/gallery/chevron.png
new file mode 100644
index 0000000000..22f569f4ec
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/chevron.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/chevron.typ b/packages/preview/cetz-plot/0.1.4/gallery/chevron.typ
new file mode 100644
index 0000000000..44ea9c0cab
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/chevron.typ
@@ -0,0 +1,89 @@
+#import "@preview/cetz:0.5.2" as cetz: draw
+#import "/src/lib.typ": smartart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let steps = (
+ [Improvise],
+ [Adapt],
+ [Overcome]
+)
+
+#let steps2 = (
+ text(fill: white)[Text here],
+ [Text here],
+ [Text here],
+ [Text here]
+).enumerate().map(((i, step)) => {
+ grid(
+ columns: 2,
+ column-gutter: 0.4em,
+ align: center + horizon,
+ box(
+ width: 2em,
+ height: 2em,
+ radius: 1em,
+ fill: white,
+ stroke: gray,
+ align(center + horizon)[*0#{i + 1}*]
+ ),
+ step
+ )
+})
+
+#cetz.canvas({
+ smartart.process.chevron(
+ steps2,
+ step-style: (
+ rgb("#352C6D"),
+ rgb("#FE7275"),
+ rgb("#8285E6"),
+ rgb("#8CDFFD")
+ ),
+ //equal-length: true,
+ dir: ltr,
+ start-cap: "(",
+ name: "chart",
+ steps: (max-width: 8em),
+ start-in-cap: true,
+ )
+})
+
+#align(
+ center + horizon,
+ stack(
+ dir: ltr,
+ spacing: 1em,
+ cetz.canvas({
+ smartart.process.chevron(
+ steps,
+ step-style: cetz.palette.light-green,
+ dir: rtl,
+ start-cap: "(",
+ end-cap: ")",
+ spacing: 4pt
+ )
+ }),
+ cetz.canvas({
+ smartart.process.chevron(
+ steps,
+ step-style: cetz.palette.orange,
+ dir: btt,
+ start-cap: "|",
+ end-cap: ")",
+ spacing: 0,
+ )
+ }),
+ cetz.canvas({
+ smartart.process.chevron(
+ steps,
+ step-style: cetz.palette.red,
+ dir: ttb,
+ start-cap: "<",
+ middle-cap: "|",
+ end-cap: ">",
+ spacing: 1em,
+ )
+ })
+ )
+)
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/circular.png b/packages/preview/cetz-plot/0.1.4/gallery/circular.png
new file mode 100644
index 0000000000..a1e735615f
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/circular.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/circular.typ b/packages/preview/cetz-plot/0.1.4/gallery/circular.typ
new file mode 100644
index 0000000000..f80f5c3108
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/circular.typ
@@ -0,0 +1,23 @@
+#import "@preview/cetz:0.5.2" as cetz: draw
+#import "/src/lib.typ": smartart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let steps = (
+ [Improvise],
+ [Adapt],
+ [Overcome],
+)
+
+#let colors = (
+ red, orange, green
+).map(c => c.lighten(40%))
+
+#cetz.canvas({
+ smartart.cycle.basic(
+ steps,
+ step-style: colors,
+ equal-width: true,
+ name: "chart",
+ )
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/cycles.png b/packages/preview/cetz-plot/0.1.4/gallery/cycles.png
new file mode 100644
index 0000000000..b4e05dc658
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/cycles.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/cycles.typ b/packages/preview/cetz-plot/0.1.4/gallery/cycles.typ
new file mode 100644
index 0000000000..2d1c6fddba
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/cycles.typ
@@ -0,0 +1,95 @@
+#import "@preview/cetz:0.5.2": canvas, draw
+#import "/src/lib.typ": smartart
+
+#set page(width: auto, height: auto, margin: .5cm)
+#set text(fill: white)
+#let steps = ([A], [B], [C], [D], [E])
+
+#let defaults() = draw.set-style(
+ cycle-basic: (
+ steps: (
+ fill: rgb("#156082"),
+ stroke: none
+ ),
+ arrows: (
+ fill: rgb("#156082"),
+ stroke: rgb("#156082")
+ )
+ )
+)
+
+#canvas({
+ defaults()
+ smartart.cycle.basic(
+ steps,
+ arrows: (
+ thickness: none
+ ),
+ step-style: none
+ )
+})
+
+#canvas({
+ defaults()
+ smartart.cycle.basic(
+ steps.map(text.with(fill: black)),
+ step-style: none,
+ steps: (shape: none)
+ )
+})
+
+#canvas({
+ defaults()
+ smartart.cycle.basic(
+ steps,
+ step-style: none,
+ arrows: (
+ fill: rgb("#AAB6C1"),
+ stroke: none
+ )
+ )
+})
+
+#canvas({
+ defaults()
+ smartart.cycle.basic(
+ steps,
+ step-style: none,
+ steps: (
+ shape: "circle"
+ ),
+ arrows: (
+ fill: rgb("#AAB6C1"),
+ stroke: none,
+ curved: false
+ )
+ )
+})
+
+#canvas({
+ defaults()
+ smartart.cycle.basic(
+ steps,
+ step-style: none,
+ arrows: (
+ fill: rgb("#AAB6C1"),
+ stroke: none,
+ curved: false,
+ double: true
+ )
+ )
+})
+
+#canvas({
+ defaults()
+ smartart.cycle.basic(
+ steps,
+ step-style: none,
+ arrows: (
+ fill: rgb("#AAB6C1"),
+ stroke: none,
+ curved: true,
+ double: true
+ )
+ )
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/line.png b/packages/preview/cetz-plot/0.1.4/gallery/line.png
new file mode 100644
index 0000000000..7e2e0dde54
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/line.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/line.typ b/packages/preview/cetz-plot/0.1.4/gallery/line.typ
new file mode 100644
index 0000000000..2edb5d26e0
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/line.typ
@@ -0,0 +1,39 @@
+#import "@preview/cetz:0.5.2": canvas, draw
+#import "@preview/cetz-plot:0.1.4": plot
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let style = (stroke: black, fill: rgb(0, 0, 200, 75))
+
+#let f1(x) = calc.sin(x)
+#let fn = (
+ ($ x - x^3"/"3! $, x => x - calc.pow(x, 3)/6),
+ ($ x - x^3"/"3! - x^5"/"5! $, x => x - calc.pow(x, 3)/6 + calc.pow(x, 5)/120),
+ ($ x - x^3"/"3! - x^5"/"5! - x^7"/"7! $, x => x - calc.pow(x, 3)/6 + calc.pow(x, 5)/120 - calc.pow(x, 7)/5040),
+)
+
+#set text(size: 10pt)
+
+#canvas({
+ import draw: *
+
+ // Set-up a thin axis style
+ set-style(axes: (stroke: .5pt, tick: (stroke: .5pt)),
+ legend: (stroke: none, orientation: ttb, item: (spacing: .3), scale: 80%))
+
+ plot.plot(size: (12, 8),
+ x-tick-step: calc.pi/2,
+ x-format: plot.formats.multiple-of,
+ y-tick-step: 2, y-min: -2.5, y-max: 2.5,
+ legend: "inner-north",
+ {
+ let domain = (-1.1 * calc.pi, +1.1 * calc.pi)
+
+ for ((title, f)) in fn {
+ plot.add-fill-between(f, f1, domain: domain,
+ style: (stroke: none), label: title)
+ }
+ plot.add(f1, domain: domain, label: $ sin x $,
+ style: (stroke: black))
+ })
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/normal-dist.png b/packages/preview/cetz-plot/0.1.4/gallery/normal-dist.png
new file mode 100644
index 0000000000..87b2406c97
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/normal-dist.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/normal-dist.typ b/packages/preview/cetz-plot/0.1.4/gallery/normal-dist.typ
new file mode 100644
index 0000000000..14c2a40e71
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/normal-dist.typ
@@ -0,0 +1,58 @@
+#import "@preview/cetz:0.5.2": canvas, draw
+#import "@preview/cetz-plot:0.1.4": plot
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let style = (stroke: black, fill: rgb(0, 0, 200, 75))
+
+#let f(x, rho: .4, sigma: 0) = 1 / calc.sqrt(2 * calc.pi * rho * rho) * calc.exp(-calc.pow(x - sigma, 2)/(2 * rho * rho))
+
+#set text(size: 10pt, fill: white)
+
+#canvas(background: gray.darken(80%), {
+ import draw: *
+
+ set-style(
+ axes: (
+ stroke: 1pt + white,
+ tick: (stroke: 1pt + white),
+ fill: gray.darken(60%),
+ minor-tick: (stroke: white),
+ grid: (stroke: (thickness: .5pt, paint: white, dash: "dotted")),
+ ),
+ legend: (
+ fill: black.transparentize(60%),
+ stroke: none,
+ padding: .3cm,
+ offset: (-.1, -.1)
+ )
+ )
+
+ let x-format = x => {
+ if x > 0 { $mu + #{x}sigma$ }
+ else if x < 0 { $mu - #{calc.abs(x)}sigma$ }
+ else { $mu$ }
+ }
+
+ plot.plot(size: (12, 8),
+ x-tick-step: 1,
+ y-tick-step: 1,
+ x-format: x-format,
+ y-max: 1.1,
+ y-min: -.1,
+ x-grid: true,
+ y-grid: true,
+ x-label: none,
+ y-label: none,
+ legend: "inner-north-east",
+ {
+ plot.add(f, domain: (-3, +3),
+ style: (stroke: green),
+ label: $y = 1/sqrt(2 pi sigma^2) exp(-(x - mu)^2/(2 sigma^2)) $,
+ samples: 200,
+ )
+ })
+
+ // Add some padding
+ rect((-1, -1), (13, 9))
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/piechart.png b/packages/preview/cetz-plot/0.1.4/gallery/piechart.png
new file mode 100644
index 0000000000..7d3468f74e
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/piechart.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/piechart.typ b/packages/preview/cetz-plot/0.1.4/gallery/piechart.typ
new file mode 100644
index 0000000000..819d6c1076
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/piechart.typ
@@ -0,0 +1,33 @@
+#import "@preview/cetz:0.5.2"
+#import "@preview/cetz-plot:0.1.4": chart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let data = (
+ ([Belgium], 24),
+ ([Germany], 31),
+ ([Greece], 18),
+ ([Spain], 21),
+ ([France], 23),
+ ([Hungary], 18),
+ ([Netherlands], 27),
+ ([Romania], 17),
+ ([Finland], 26),
+ ([Turkey], 13),
+)
+
+#cetz.canvas({
+ let colors = gradient.linear(red, blue, green, yellow)
+
+ chart.piechart(
+ data,
+ value-key: 1,
+ label-key: none,
+ radius: 4,
+ stroke: none,
+ slice-style: colors,
+ inner-radius: 1,
+ outset: 3,
+ inner-label: (content: (value, label) => [#text(white, str(value))], radius: 110%),
+ outer-label: (content: "%", radius: 110%))
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/process.png b/packages/preview/cetz-plot/0.1.4/gallery/process.png
new file mode 100644
index 0000000000..d457e68666
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/process.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/process.typ b/packages/preview/cetz-plot/0.1.4/gallery/process.typ
new file mode 100644
index 0000000000..c522223e5c
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/process.typ
@@ -0,0 +1,34 @@
+#import "@preview/cetz:0.5.2" as cetz: draw
+#import "/src/lib.typ": smartart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let steps = (
+ [Improvise],
+ [Adapt],
+ [Overcome]
+)
+
+#let colors = (
+ red, orange, green
+).map(c => c.lighten(40%))
+
+#cetz.canvas({
+ smartart.process.basic(
+ steps,
+ step-style: colors,
+ equal-width: true,
+ dir: ltr,
+ name: "chart",
+ )
+})
+
+#cetz.canvas({
+ smartart.process.chevron(
+ steps,
+ step-style: colors,
+ equal-length: true,
+ dir: ltr,
+ name: "chart",
+ )
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/pyramid.png b/packages/preview/cetz-plot/0.1.4/gallery/pyramid.png
new file mode 100644
index 0000000000..4e556ce6f9
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/pyramid.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/pyramid.typ b/packages/preview/cetz-plot/0.1.4/gallery/pyramid.typ
new file mode 100644
index 0000000000..bc0e75abbc
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/pyramid.typ
@@ -0,0 +1,37 @@
+#import "@preview/cetz:0.5.2"
+#import "@preview/cetz-plot:0.1.4": chart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#let data = (
+ ([Cash], 768),
+ ([Funds], 1312),
+ ([Stocks], 3812),
+ ([Bonds], 7167),
+)
+#let total = data.map(i => i.last()).sum()
+
+#cetz.canvas({
+ let colors = gradient.linear(red, yellow)
+
+ chart.pyramid(
+ data,
+ value-key: 1,
+ label-key: 0,
+ mode: "AREA-HEIGHT",
+ stroke: none,
+ level-style: colors,
+ inner-label: (
+ content: (value, label) => align(center, stack(
+ label + "\n",
+ str(calc.round(value / total * 10000) / 100) + "%",
+ spacing: 2pt,
+ dir: ttb
+ ))
+ ),
+ side-label: (
+ content: (value, label) => "$" + str(value)
+ ),
+ gap: 10%
+ )
+})
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/radarchart.png b/packages/preview/cetz-plot/0.1.4/gallery/radarchart.png
new file mode 100644
index 0000000000..15bf3e7bb9
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/gallery/radarchart.png differ
diff --git a/packages/preview/cetz-plot/0.1.4/gallery/radarchart.typ b/packages/preview/cetz-plot/0.1.4/gallery/radarchart.typ
new file mode 100644
index 0000000000..99b3521558
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/gallery/radarchart.typ
@@ -0,0 +1,28 @@
+#import "@preview/cetz:0.5.2"
+#import "/src/lib.typ": chart
+
+#set page(width: auto, height: auto, margin: .5cm)
+
+#cetz.canvas({
+ chart.radarchart(
+ (
+ [A],
+ [B],
+ [C],
+ [D],
+ [E],
+ [F],
+ ),
+ (
+ (0.3, 1, 0.3, 0.8, 0.8, 1),
+ (0.9, 0.3, 0.9, 0.5, 0.5, 0.4),
+ ),
+ radius: 3,
+ web-label-offset: 0.6,
+ data-style: (
+ blue.transparentize(10%),
+ red.transparentize(30%),
+ ),
+ )
+})
+
diff --git a/packages/preview/cetz-plot/0.1.4/manual.pdf b/packages/preview/cetz-plot/0.1.4/manual.pdf
new file mode 100644
index 0000000000..20d9e7d20a
Binary files /dev/null and b/packages/preview/cetz-plot/0.1.4/manual.pdf differ
diff --git a/packages/preview/cetz-plot/0.1.4/manual.typ b/packages/preview/cetz-plot/0.1.4/manual.typ
new file mode 100644
index 0000000000..f7b5c58a3f
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/manual.typ
@@ -0,0 +1,121 @@
+#import "/src/cetz.typ"
+#import "/doc/util.typ": *
+#import "/doc/example.typ": example
+#import "/doc/style.typ" as doc-style
+#import "/src/lib.typ": *
+#import "@preview/tidy:0.4.3"
+
+
+// Usage:
+// ```cexample
+// /* canvas drawing code */
+// ```
+//
+// Why cexample? Because tidy thinks it has to mess
+// with each raw block...
+#show raw.where(lang: "cexample"): example
+#show raw.where(lang: "cexample-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: auto, 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.5.2"
+#import "@preview/cetz-plot:0.1.4"
+#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",
+ "legend",
+) {
+ doc-style.parse-show-module("/src/plot/" + m + ".typ")
+}
+
+== Styling
+You can use style root `axes` with the following keys:
+#doc-style.parse-show-module("/src/axes.typ")
+
+=== Example
+```cexample
+import cetz.draw: *
+import cetz-plot: *
+
+set-style(axes: (
+ stroke: (dash: "dotted", paint: gray),
+ x: (mark: (start: ">", end: ">"), padding: 1),
+ y: (mark: none),
+ tick: (stroke: gray + .5pt),
+))
+
+plot.plot(size: (5, 4), axis-style: "school-book", y-tick-step: none, {
+ plot.add(calc.sin, domain: (0, calc.pi * 2))
+})
+
+```
+
+= Chart
+
+#doc-style.parse-show-module("/src/chart.typ")
+#for m in (
+ "barchart",
+ "boxwhisker",
+ "columnchart",
+ "piechart",
+ "radarchart",
+ "pyramid",
+) {
+ doc-style.parse-show-module("/src/chart/" + m + ".typ")
+}
+
+= SmartArt
+
+#doc-style.parse-show-module("/src/smartart/common.typ")
+
+== Process
+
+#doc-style.parse-show-module("/src/smartart/process.typ")
+
+== Cycle
+
+#doc-style.parse-show-module("/src/smartart/cycle.typ")
diff --git a/packages/preview/cetz-plot/0.1.4/src/axes.typ b/packages/preview/cetz-plot/0.1.4/src/axes.typ
new file mode 100644
index 0000000000..8b1f3efc52
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/axes.typ
@@ -0,0 +1,921 @@
+#import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process
+#import "/src/plot/formats.typ"
+
+/// Default axis style
+///
+/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.])
+/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.])
+/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.])
+/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.])
+/// #show-parameter-block("stroke", "stroke", [Axis stroke style.])
+/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.])
+/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.])
+/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.])
+/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see cetz' `on-layer`)])
+/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see cetz' `on-layer`)])
+/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see cetz' `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,
+))
+
+// Default Schoolbook Style
+#let default-style-schoolbook = util.merge-dictionary(default-style, (
+ x: (stroke: auto, fill: none, mark: (start: none, end: "straight"),
+ tick: (label: (anchor: "north"))),
+ y: (stroke: auto, fill: none, mark: (start: none, end: "straight"),
+ tick: (label: (anchor: "east"))),
+ label: (offset: .1cm),
+ origin: (label: (offset: .05cm)),
+ padding: .1cm, // Axis padding on both sides outsides the plotting area
+ overshoot: .5cm, // Axis end "overshoot" out of the plotting area
+ tick: (
+ offset: -50%,
+ minor-offset: -50%,
+ length: .2cm,
+ minor-length: 70%,
+ ),
+ shared-zero: $0$, // Show zero tick label at (0, 0)
+))
+
+#let _prepare-style(ctx, style) = {
+ if type(style) != dictionary { return style }
+
+ let res = util.resolve-number.with(ctx)
+ let rel-to(v, to) = {
+ if type(v) == ratio {
+ return v * to / 100%
+ } else {
+ return res(v)
+ }
+ }
+
+ style.tick.length = res(style.tick.length)
+ style.tick.offset = rel-to(style.tick.offset, style.tick.length)
+ style.tick.minor-length = rel-to(style.tick.minor-length, style.tick.length)
+ style.tick.minor-offset = rel-to(style.tick.minor-offset, style.tick.minor-length)
+ style.tick.label.offset = res(style.tick.label.offset)
+
+ // Break points
+ style.break-point.width = res(style.break-point.width)
+ style.break-point.length = res(style.break-point.length)
+
+ // Padding
+ style.padding = res(style.padding)
+
+ if "overshoot" in style {
+ style.overshoot = res(style.overshoot)
+ }
+
+ return style
+}
+
+#let _get-axis-style(ctx, style, name) = {
+ if not name in style {
+ return style
+ }
+
+ style = styles.resolve(style, merge: style.at(name))
+ return _prepare-style(ctx, style)
+}
+
+#let _get-grid-type(axis) = {
+ let grid = axis.ticks.at("grid", default: false)
+ if grid == "major" or grid == true { return 1 }
+ if grid == "minor" { return 2 }
+ if grid == "both" { return 3 }
+ return 0
+}
+
+#let _inset-axis-points(ctx, style, axis, start, end) = {
+ if axis == none { return (start, end) }
+
+ let (low, high) = axis.inset.map(v => util.resolve-number(ctx, v))
+
+ let is-horizontal = start.at(1) == end.at(1)
+ if is-horizontal {
+ start = vector.add(start, (low, 0))
+ end = vector.sub(end, (high, 0))
+ } else {
+ start = vector.add(start, (0, low))
+ end = vector.sub(end, (0, high))
+ }
+ return (start, end)
+}
+
+#let _draw-axis-line(start, end, axis, is-horizontal, style) = {
+ let enabled = if axis != none and axis.show-break {
+ axis.min > 0 or axis.max < 0
+ } else { false }
+
+ if enabled {
+ let size = if is-horizontal {
+ (style.break-point.width, 0)
+ } else {
+ (0, style.break-point.width, 0)
+ }
+
+ let up = if is-horizontal {
+ (0, style.break-point.length)
+ } else {
+ (style.break-point.length, 0)
+ }
+
+ let add-break(is-end) = {
+ let a = ()
+ let b = (rel: vector.scale(size, .3), update: false)
+ let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false)
+ let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false)
+ let e = (rel: vector.scale(size, .7), update: false)
+ let f = (rel: size)
+
+ let mark = if is-end {
+ style.at("mark", default: none)
+ }
+ draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark)
+ }
+
+ draw.merge-path({
+ draw.move-to(start)
+ if axis.min > 0 {
+ add-break(false)
+ draw.line((rel: size, to: start), end)
+ } else if axis.max < 0 {
+ draw.line(start, (rel: vector.scale(size, -1), to: end))
+ add-break(true)
+ }
+ }, stroke: style.stroke, mark: style.at("mark", default: none))
+ } else {
+ draw.line(start, end, stroke: style.stroke, mark: style.at("mark", default: none))
+ }
+}
+
+// Construct Axis Object
+//
+// - min (number): Minimum value
+// - max (number): Maximum value
+// - ticks (dictionary): Tick settings:
+// - step (number): Major tic step
+// - minor-step (number): Minor tic step
+// - decimals (int): Tick float decimal length
+// - label (content): Axis label
+// - mode (string): Axis scaling function. Takes `lin` or `log`
+// - base (number): Base for tick labels when logarithmically scaled.
+#let axis(min: -1, max: 1, label: none,
+ ticks: (step: auto, minor-step: none,
+ decimals: 2, grid: false,
+ format: "float"
+ ),
+ mode: auto, base: auto) = (
+ min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, mode: mode, base: base
+)
+
+// Format a tick value
+#let format-tick-value(value, tic-options) = {
+ // Without it we get negative zero in conversion
+ // to content! Typst has negative zero floats.
+ if value == 0 { value = 0 }
+
+ if type(value) != std.content {
+ let format = tic-options.at("format", default: "float")
+ if format == none {
+ value = []
+ } else if type(format) == std.content {
+ value = format
+ } else if type(format) == function {
+ value = (format)(value)
+ } else if format == "sci" {
+ value = formats.sci(value, digits: tic-options.at("decimals", default: 2))
+ } else {
+ value = formats.decimal(value, digits: tic-options.at("decimals", default: 2))
+ }
+ } else if type(value) != std.content {
+ value = str(value)
+ }
+
+ return value
+}
+
+// Get value on axis [0, 1]
+//
+// - axis (axis): Axis
+// - v (number): Value
+// -> float
+#let value-on-axis(axis, v) = {
+ if v == none { return }
+ let (min, max) = (axis.min, axis.max)
+ if axis.mode == "log" {
+ min = calc.log(min)
+ max = calc.log(max)
+ v = calc.log(v)
+ }
+
+ let dt = max - min; if dt == 0 { dt = 1 }
+ return (v - min) / dt
+}
+
+// Compute list of linear ticks for axis
+//
+// - axis (axis): Axis
+#let compute-linear-ticks(axis, style, add-zero: true) = {
+ let (min, max) = (axis.min, axis.max)
+ let dt = max - min; if (dt == 0) { dt = 1 }
+ let ticks = axis.ticks
+ let ferr = util.float-epsilon
+ let tick-limit = style.tick-limit
+ let minor-tick-limit = style.minor-tick-limit
+
+ let l = ()
+ if ticks != none {
+ let major-tick-values = ()
+ if "step" in ticks and ticks.step != none {
+ assert(ticks.step >= 0,
+ message: "Axis tick step must be positive and non 0.")
+ if axis.min > axis.max { ticks.step *= -1 }
+
+ let s = 1 / ticks.step
+
+ let num-ticks = int(max * s + 1.5) - int(min * s)
+ assert(num-ticks <= tick-limit,
+ message: "Number of major ticks exceeds limit " + str(tick-limit))
+
+ let n = range(int(min * s), int(max * s + 1.5))
+ for t in n {
+ let v = (t / s - min) / dt
+ if t / s == 0 and not add-zero { continue }
+
+ if v >= 0 - ferr and v <= 1 + ferr {
+ l.push((v, format-tick-value(t / s, ticks), true))
+ major-tick-values.push(v)
+ }
+ }
+ }
+
+ if "minor-step" in ticks and ticks.minor-step != none {
+ assert(ticks.minor-step >= 0,
+ message: "Axis minor tick step must be positive")
+ if axis.min > axis.max { ticks.minor-step *= -1 }
+
+ let s = 1 / ticks.minor-step
+
+ let num-ticks = int(max * s + 1.5) - int(min * s)
+ assert(num-ticks <= minor-tick-limit,
+ message: "Number of minor ticks exceeds limit " + str(minor-tick-limit))
+
+ let n = range(int(min * s), int(max * s + 1.5))
+ for t in n {
+ let v = (t / s - min) / dt
+ if v in major-tick-values {
+ // Prefer major ticks over minor ticks
+ continue
+ }
+
+ if v != none and v >= 0 and v <= 1 + ferr {
+ l.push((v, none, false))
+ }
+ }
+ }
+
+ }
+
+ return l
+}
+
+// Compute list of linear ticks for axis
+//
+// - axis (axis): Axis
+#let compute-logarithmic-ticks(axis, style, add-zero: true) = {
+ let ferr = util.float-epsilon
+ let (min, max) = (
+ calc.log(calc.max(axis.min, ferr), base: axis.base),
+ calc.log(calc.max(axis.max, ferr), base: axis.base)
+ )
+ let dt = max - min; if (dt == 0) { dt = 1 }
+ let ticks = axis.ticks
+
+ let tick-limit = style.tick-limit
+ let minor-tick-limit = style.minor-tick-limit
+ let l = ()
+
+ if ticks != none {
+ let major-tick-values = ()
+ if "step" in ticks and ticks.step != none {
+ assert(ticks.step >= 0,
+ message: "Axis tick step must be positive and non 0.")
+ if axis.min > axis.max { ticks.step *= -1 }
+
+ let s = 1 / ticks.step
+
+ let num-ticks = int(max * s + 1.5) - int(min * s)
+ assert(num-ticks <= tick-limit,
+ message: "Number of major ticks exceeds limit " + str(tick-limit))
+
+ let n = range(
+ int(min * s),
+ int(max * s + 1.5)
+ )
+
+ for t in n {
+ let v = (t / s - min) / dt
+ if t / s == 0 and not add-zero { continue }
+
+ if v >= 0 - ferr and v <= 1 + ferr {
+ l.push((v, format-tick-value( calc.pow(axis.base, t / s), ticks), true))
+ major-tick-values.push(v)
+ }
+ }
+ }
+
+ if "minor-step" in ticks and ticks.minor-step != none {
+ assert(ticks.minor-step >= 0,
+ message: "Axis minor tick step must be positive")
+ if axis.min > axis.max { ticks.minor-step *= -1 }
+
+ let s = 1 / ticks.step
+ let n = range(int(min * s)-1, int(max * s + 1.5)+1)
+
+ for t in n {
+ for vv in range(1, int(axis.base / ticks.minor-step)) {
+
+ let v = ( (calc.log(vv * ticks.minor-step, base: axis.base) + t)/ s - min) / dt
+ if v in major-tick-values {continue}
+
+ if v != none and v >= 0 and v <= 1 + ferr {
+ l.push((v, none, false))
+ }
+
+ }
+
+ }
+ }
+ }
+
+ return l
+}
+
+// Get list of fixed axis ticks
+//
+// - axis (axis): Axis object
+#let fixed-ticks(axis) = {
+ let l = ()
+ if "list" in axis.ticks {
+ for t in axis.ticks.list {
+ let (v, label) = (none, none)
+ if type(t) in (float, int) {
+ v = t
+ label = format-tick-value(t, axis.ticks)
+ } else {
+ (v, label) = t
+ }
+
+ v = value-on-axis(axis, v)
+ if v != none and v >= 0 and v <= 1 {
+ l.push((v, label, true))
+ }
+ }
+ }
+ return l
+}
+
+// Compute list of axis ticks
+//
+// A tick triple has the format:
+// (rel-value: float, label: content, major: bool)
+//
+// - axis (axis): Axis object
+#let compute-ticks(axis, style, add-zero: true) = {
+ let find-max-n-ticks(axis, n: 11) = {
+ let dt = calc.abs(axis.max - axis.min)
+ let scale = calc.floor(calc.log(dt, base: 10) - 1)
+ if scale > 5 or scale < -5 {return none}
+
+ let (step, best) = (none, 0)
+ for s in style.auto-tick-factors {
+ s = s * calc.pow(10, scale)
+
+ let divs = calc.abs(dt / s)
+ if divs >= best and divs <= n {
+ step = s
+ best = divs
+ }
+ }
+ return step
+ }
+
+ if axis == none or axis.ticks == none { return () }
+ if axis.ticks.step == auto {
+ axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count)
+ }
+ if axis.ticks.minor-step == auto {
+ axis.ticks.minor-step = if axis.ticks.step != none {
+ axis.ticks.step / 5
+ } else {
+ none
+ }
+ }
+
+ let ticks = if axis.mode == "log" {
+ compute-logarithmic-ticks(axis, style, add-zero: add-zero)
+ } else {
+ compute-linear-ticks(axis, style, add-zero: add-zero)
+ }
+ ticks += fixed-ticks(axis)
+ return ticks
+}
+
+// Prepares the axis post creation. The given axis
+// must be completely set-up, including its interval.
+// Returns the prepared axis
+#let prepare-axis(ctx, axis, name) = {
+ let style = styles.resolve(ctx.style, root: "axes",
+ base: default-style-scientific)
+ style = _prepare-style(ctx, style)
+ style = _get-axis-style(ctx, style, name)
+
+ if type(axis.inset) != array {
+ axis.inset = (axis.inset, axis.inset)
+ }
+
+ axis.inset = axis.inset.map(v => util.resolve-number(ctx, v))
+
+ if axis.show-break {
+ if axis.min > 0 {
+ axis.inset.at(0) += style.break-point.width
+ } else if axis.max < 0 {
+ axis.inset.at(1) += style.break-point.width
+ }
+ }
+
+ return axis
+}
+
+// Transform a single 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 axes = (x-axis, y-axis)
+
+ 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, z, body, name: none) = {
+ draw.group(name: name, (ctx => {
+ let transform = ctx.transform
+
+ ctx.transform = matrix.ident(4)
+ let (ctx, drawables, bounds) = process.many(ctx, util.resolve-body(ctx, body))
+
+ ctx.transform = transform
+
+ let transform-vec = transform-vec.with(size, x, y, none)
+
+ drawables = drawables.map(d => {
+ if "segments" in d {
+ d.segments = d.segments.map(((origin, closed, elements)) => {
+ elements = elements.map(((kind, ..pts)) => {
+ (kind, ..(pts.map(transform-vec)))
+ })
+ (transform-vec(origin), closed, elements)
+ })
+ }
+ if "pos" in d {
+ d.pos = transform-vec(d.pos)
+ }
+ return d
+ })
+
+ return (
+ ctx: ctx,
+ drawables: drawable.apply-transform(ctx.transform, drawables)
+ )
+ },))
+}
+
+// Draw grid lines for the ticks of an axis
+//
+// - cxt (context):
+// - axis (dictionary): The axis
+// - ticks (array): The computed ticks
+// - low (vector): Start position of a grid-line at tick 0
+// - high (vector): End position of a grid-line at tick 0
+// - dir (vector): Normalized grid direction vector along the grid axis
+// - style (style): Axis style
+#let draw-grid-lines(ctx, axis, ticks, low, high, dir, style) = {
+ let offset = (0,0)
+ if axis.inset != none {
+ let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v))
+ offset = vector.scale(vector.norm(dir), inset-low)
+ dir = vector.sub(dir, vector.scale(vector.norm(dir), inset-low + inset-high))
+ }
+
+ let kind = _get-grid-type(axis)
+ if kind > 0 {
+ for (distance, label, is-major) in ticks {
+ let offset = vector.add(vector.scale(dir, distance), offset)
+ let start = vector.add(low, offset)
+ let end = vector.add(high, offset)
+
+ // Draw a major line
+ if is-major and (kind == 1 or kind == 3) {
+ draw.line(start, end, stroke: style.grid.stroke)
+ }
+ // Draw a minor line
+ if not is-major and kind >= 2 {
+ draw.line(start, end, stroke: style.minor-grid.stroke)
+ }
+ }
+ }
+}
+
+// Place a list of tick marks and labels along a path
+#let place-ticks-on-line(ticks, start, stop, style, flip: false, is-mirror: false) = {
+ let dir = vector.sub(stop, start)
+ let norm = vector.norm((-dir.at(1), dir.at(0), dir.at(2, default: 0)))
+
+ let def(v, d) = {
+ return if v == none or v == auto {d} else {v}
+ }
+
+ let show-label = style.tick.label.show
+ if show-label == auto {
+ show-label = not is-mirror
+ }
+
+ for (distance, label, is-major) in ticks {
+ let offset = style.tick.offset
+ let length = if is-major { style.tick.length } else { style.tick.minor-length }
+ if flip {
+ offset *= -1
+ length *= -1
+ }
+
+ let pt = vector.lerp(start, stop, distance)
+ let a = vector.add(pt, vector.scale(norm, offset))
+ let b = vector.add(a, vector.scale(norm, length))
+
+ draw.line(a, b, stroke: style.tick.stroke)
+
+ if show-label and label != none {
+ let offset = style.tick.label.offset
+ if flip {
+ offset *= -1
+ length *= -1
+ }
+
+ let c = vector.sub(if length <= 0 { b } else { a },
+ vector.scale(norm, offset))
+
+ let angle = def(style.tick.label.angle, 0deg)
+ let anchor = def(style.tick.label.anchor, "center")
+
+ draw.content(c, [#label], angle: angle, anchor: anchor)
+ }
+ }
+}
+
+// Draw up to four axes in an "scientific" style at origin (0, 0)
+//
+// - size (array): Size (width, height)
+// - left (axis): Left (y) axis
+// - bottom (axis): Bottom (x) axis
+// - right (axis): Right axis
+// - top (axis): Top axis
+// - name (string): Object name
+// - draw-unset (bool): Draw axes that are set to `none`
+// - ..style (any): Style
+#let scientific(size: (1, 1),
+ left: none,
+ right: auto,
+ bottom: none,
+ top: auto,
+ draw-unset: true,
+ name: none,
+ ..style) = {
+ import draw: *
+
+ if right == auto {
+ if left != none {
+ right = left; right.is-mirror = true
+ } else {
+ right = none
+ }
+ }
+ if top == auto {
+ if bottom != none {
+ top = bottom; top.is-mirror = true
+ } else {
+ top = none
+ }
+ }
+
+ group(name: name, ctx => {
+ let (w, h) = size
+ anchor("origin", (0, 0))
+
+ let style = style.named()
+ style = styles.resolve(ctx.style, merge: style, root: "axes",
+ base: default-style-scientific)
+ style = _prepare-style(ctx, style)
+
+ // Compute ticks
+ let x-ticks = compute-ticks(bottom, style)
+ let y-ticks = compute-ticks(left, style)
+ let x2-ticks = compute-ticks(top, style)
+ let y2-ticks = compute-ticks(right, style)
+
+ // Draw frame
+ if style.fill != none {
+ on-layer(style.background-layer, {
+ rect((0,0), (w,h), fill: style.fill, stroke: none)
+ })
+ }
+
+ // Draw grid
+ group(name: "grid", ctx => {
+ let axes = (
+ ("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom),
+ ("top", (0,h), (0,0), (+w,0), x2-ticks, top),
+ ("left", (0,0), (w,0), (0,+h), y-ticks, left),
+ ("right", (w,0), (0,0), (0,+h), y2-ticks, right),
+ )
+ for (name, start, end, direction, ticks, axis) in axes {
+ if axis == none { continue }
+
+ let style = _get-axis-style(ctx, style, name)
+ let is-mirror = axis.at("is-mirror", default: false)
+
+ if not is-mirror {
+ on-layer(style.grid-layer, {
+ draw-grid-lines(ctx, axis, ticks, start, end, direction, style)
+ })
+ }
+ }
+ })
+
+ // Draw axes
+ group(name: "axes", {
+ let axes = (
+ ("bottom", (0, 0), (w, 0), (0, -1), false, x-ticks, bottom,),
+ ("top", (0, h), (w, h), (0, +1), true, x2-ticks, top,),
+ ("left", (0, 0), (0, h), (-1, 0), true, y-ticks, left,),
+ ("right", (w, 0), (w, h), (+1, 0), false, y2-ticks, right,)
+ )
+ let label-placement = (
+ bottom: ("south", "north", 0deg),
+ top: ("north", "south", 0deg),
+ left: ("west", "south", 90deg),
+ right: ("east", "north", 90deg),
+ )
+
+ for (name, start, end, outsides, flip, ticks, axis) in axes {
+ let style = _get-axis-style(ctx, style, name)
+ let is-mirror = axis == none or axis.at("is-mirror", default: false)
+ let is-horizontal = name in ("bottom", "top")
+
+ if style.padding != 0 {
+ let padding = vector.scale(outsides, style.padding)
+ start = vector.add(start, padding)
+ end = vector.add(end, padding)
+ }
+
+ let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end)
+
+ let path = _draw-axis-line(start, end, axis, is-horizontal, style)
+ on-layer(style.axis-layer, {
+ group(name: "axis", {
+ if draw-unset or axis != none {
+ path;
+ place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror)
+ }
+ })
+
+ if axis != none and axis.label != none and not is-mirror {
+ let offset = vector.scale(outsides, style.label.offset)
+ let (group-anchor, content-anchor, angle) = label-placement.at(name)
+
+ if style.label.anchor != auto {
+ content-anchor = style.label.anchor
+ }
+ if style.label.angle != auto {
+ angle = style.label.angle
+ }
+
+ content((rel: offset, to: "axis." + group-anchor),
+ [#axis.label],
+ angle: angle,
+ anchor: content-anchor)
+ }
+ })
+ }
+ })
+ })
+}
+
+// Draw two axes in a "school book" style
+//
+// - x-axis (axis): X axis
+// - y-axis (axis): Y axis
+// - size (array): Size (width, height)
+// - x-position (number): X Axis position
+// - y-position (number): Y Axis position
+// - name (string): Object name
+// - ..style (any): Style
+#let school-book(x-axis, y-axis,
+ size: (1, 1),
+ x-position: 0,
+ y-position: 0,
+ name: none,
+ ..style) = {
+ import draw: *
+
+ group(name: name, ctx => {
+ let (w, h) = size
+ anchor("origin", (0, 0))
+
+ let style = style.named()
+ style = styles.resolve(
+ ctx.style,
+ merge: style,
+ root: "axes",
+ base: default-style-schoolbook)
+ style = _prepare-style(ctx, style)
+
+ let x-position = calc.min(calc.max(y-axis.min, x-position), y-axis.max)
+ let y-position = calc.min(calc.max(x-axis.min, y-position), x-axis.max)
+ let x-y = value-on-axis(y-axis, x-position) * h
+ let y-x = value-on-axis(x-axis, y-position) * w
+
+ let shared-zero = style.shared-zero != false and x-position == 0 and y-position == 0
+
+ let x-ticks = compute-ticks(x-axis, style, add-zero: not shared-zero)
+ let y-ticks = compute-ticks(y-axis, style, add-zero: not shared-zero)
+
+ // Draw grid
+ group(name: "grid", ctx => {
+ let axes = (
+ ("x", (0,0), (0,h), (+w,0), x-ticks, x-axis),
+ ("y", (0,0), (w,0), (0,+h), y-ticks, y-axis),
+ )
+
+ for (name, start, end, direction, ticks, axis) in axes {
+ if axis == none { continue }
+
+ let style = _get-axis-style(ctx, style, name)
+ on-layer(style.grid-layer, {
+ draw-grid-lines(ctx, axis, ticks, start, end, direction, style)
+ })
+ }
+ })
+
+ // Draw axes
+ group(name: "axes", {
+ let axes = (
+ ("x", (0, x-y), (w, x-y), (1, 0), false, x-ticks, x-axis),
+ ("y", (y-x, 0), (y-x, h), (0, 1), true, y-ticks, y-axis),
+ )
+ let label-pos = (
+ x: ("north", (0,-1)),
+ y: ("east", (-1,0)),
+ )
+
+ on-layer(style.axis-layer, {
+ for (name, start, end, dir, flip, ticks, axis) in axes {
+ let style = _get-axis-style(ctx, style, name)
+
+ let pad = style.padding
+ let overshoot = style.overshoot
+ let vstart = vector.sub(start, vector.scale(dir, pad))
+ let vend = vector.add(end, vector.scale(dir, pad + overshoot))
+ let is-horizontal = name == "x"
+
+ let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end)
+ group(name: "axis", {
+ _draw-axis-line(vstart, vend, axis, is-horizontal, style)
+ place-ticks-on-line(ticks, data-start, data-end, style, flip: flip)
+ })
+
+ if axis.label != none {
+ let (content-anchor, offset-dir) = label-pos.at(name)
+
+ let angle = if style.label.angle not in (none, auto) {
+ style.label.angle
+ } else { 0deg }
+ if style.label.anchor not in (none, auto) {
+ content-anchor = style.label.anchor
+ }
+
+ let offset = vector.scale(offset-dir, style.label.offset)
+ content((rel: offset, to: vend),
+ [#axis.label],
+ angle: angle,
+ anchor: content-anchor)
+ }
+ }
+
+ if shared-zero {
+ let pt = (rel: (-style.tick.label.offset, -style.tick.label.offset),
+ to: (y-x, x-y))
+ let zero = if type(style.shared-zero) == std.content {
+ style.shared-zero
+ } else {
+ $0$
+ }
+ content(pt, zero, anchor: "north-east")
+ }
+ })
+ })
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/axis.typ b/packages/preview/cetz-plot/0.1.4/src/axis.typ
new file mode 100644
index 0000000000..6d43446ebc
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/axis.typ
@@ -0,0 +1,25 @@
+#let lin-axis(name, min, max) = {
+ (name: name,
+ mode: "lin",
+ min: min,
+ max: max,)
+}
+
+#let log-axis(name, min, max, base: 10) = {
+ (name: name,
+ mode: "log",
+ min: min,
+ max: max,
+ base: base,)
+}
+
+///
+#let setup-cartesian-axis(ctx, axis, begin, end) = {
+ if not "plot-axes" in ctx {
+ ctx.cetz-plot-axes = (:)
+ }
+
+ ctx.cetz-plot-axes.insert(axis.name, axis)
+
+ return ctx
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/cetz.typ b/packages/preview/cetz-plot/0.1.4/src/cetz.typ
new file mode 100644
index 0000000000..8a95d8fd9e
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/cetz.typ
@@ -0,0 +1,2 @@
+// Import cetz into the root scope. Import cetz by importing this file only!
+#import "@preview/cetz:0.5.2": *
diff --git a/packages/preview/cetz-plot/0.1.4/src/chart.typ b/packages/preview/cetz-plot/0.1.4/src/chart.typ
new file mode 100644
index 0000000000..c9344a7d3c
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart.typ
@@ -0,0 +1,6 @@
+#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
+#import "chart/radarchart.typ": radarchart, radarchart-default-style
+#import "chart/pyramid.typ": pyramid, pyramid-default-style
diff --git a/packages/preview/cetz-plot/0.1.4/src/chart/barchart.typ b/packages/preview/cetz-plot/0.1.4/src/chart/barchart.typ
new file mode 100644
index 0000000000..79c9f3eb70
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart/barchart.typ
@@ -0,0 +1,145 @@
+#import "/src/cetz.typ": draw, styles, palette
+
+#import "/src/plot.typ"
+
+#let barchart-default-style = (
+ axes: (tick: (length: 0), grid: (stroke: (dash: "dotted", paint: gray))),
+ 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
+/// Can be applied with `cetz.draw.set-style(barchart: (bar-width: 1))`.
+///
+/// *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.])
+/// #show-parameter-block("cluster-gap", "float", default: 0, [
+/// Spacing between bars insides a cluster.])
+/// 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,array): 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-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-format: 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-format = x-format
+ if x-format == auto {
+ x-format = if mode == "stacked100" {plot.formats.decimal.with(suffix: [%])} else {auto}
+ }
+
+ 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,
+ x-format: x-format,
+ 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/packages/preview/cetz-plot/0.1.4/src/chart/barcol-common.typ b/packages/preview/cetz-plot/0.1.4/src/chart/barcol-common.typ
new file mode 100644
index 0000000000..0c09a52604
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart/barcol-common.typ
@@ -0,0 +1,40 @@
+// 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/packages/preview/cetz-plot/0.1.4/src/chart/boxwhisker.typ b/packages/preview/cetz-plot/0.1.4/src/chart/boxwhisker.typ
new file mode 100644
index 0000000000..ffb9e8af62
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart/boxwhisker.typ
@@ -0,0 +1,97 @@
+#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", paint: gray))),
+ box-width: 0.75,
+ whisker-width: 0.5,
+ mark-size: 0.15,
+)
+
+/// Add one or more box or whisker plots.
+///
+/// ```cexample
+/// 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/packages/preview/cetz-plot/0.1.4/src/chart/columnchart.typ b/packages/preview/cetz-plot/0.1.4/src/chart/columnchart.typ
new file mode 100644
index 0000000000..5a9f4ea852
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart/columnchart.typ
@@ -0,0 +1,140 @@
+#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,array): 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-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-format: 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-format = y-format
+ if y-format == auto {
+ y-format = if mode == "stacked100" {plot.formats.decimal.with(suffix: [%])} else {auto}
+ }
+
+ 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,
+ y-format: y-format,
+ 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/packages/preview/cetz-plot/0.1.4/src/chart/piechart.typ b/packages/preview/cetz-plot/0.1.4/src/chart/piechart.typ
new file mode 100644
index 0000000000..e32fcac35f
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart/piechart.typ
@@ -0,0 +1,495 @@
+#import "/src/cetz.typ": draw, styles, palette, util, vector, intersection
+#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
+///
+/// ```cexample
+/// 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 visible-data = data.filter(((value, ..)) => value != 0)
+
+ let sum = visible-data.map(((value, ..)) => value).sum()
+ if sum == 0 {
+ sum = 1
+ }
+
+ group(name: name, ctx => {
+ anchor("default", (0,0))
+
+ let style = styles.resolve(ctx.style,
+ 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 / visible-data.len(),
+ message: "Gap angle is too big for " + str(visible-data.len()) + "items. Maximum gap angle: " + repr(360deg / visible-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 / visible-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.filter((value, ..) => value != 0) {
+ 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
+ }
+
+ let circle-arclen(radius, angle: 90deg) = {
+ calc.abs(angle / 360deg * 2 * calc.pi * radius)
+ }
+
+ // 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 visible-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)))
+ }
+
+ on-layer(1, 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/packages/preview/cetz-plot/0.1.4/src/chart/pyramid.typ b/packages/preview/cetz-plot/0.1.4/src/chart/pyramid.typ
new file mode 100644
index 0000000000..8b008ad22a
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart/pyramid.typ
@@ -0,0 +1,467 @@
+#import "/src/cetz.typ": draw, styles, palette, coordinate
+
+// Pyramid Chart Label Kind
+#let label-kind = (value: "VALUE", percentage: "%", label: "LABEL")
+
+// Pyramid Chart Default Style
+#let default-style = (
+ stroke: auto,
+ fill: auto,
+ /// Gap between levels to leave empty.
+ /// If `mode` is "AREA-HEIGHT", the value must be a ratio and will be proportional to the height of the first level
+ gap: 0,
+ /// Pyramid mode defining how to shape each level:
+ /// - "REGULAR": All levels have the same height and make a perfectly triangular pyramid
+ /// - "AREA-HEIGHT": The area of each level is proportional to its value. Only the height is adapted, keeping the pyramid triangular
+ /// - "HEIGHT": The height of each level is proportional to its value. The pyramid is kept as a perfect triangle
+ /// - "WIDTH": The height of each level is fixed, but its width is proportional to the value. The pyramid might not be perfectly triangular
+ mode: "REGULAR",
+ /// Height of each level
+ level-height: 1,
+ inner-label: (
+ /// Label kind
+ /// If set to a function, that function gets called with (value, label) of each item
+ content: "LABEL",
+ force-inside: false
+ ),
+ side-label: (
+ /// Label kind
+ /// If set to a function, that function gets called with (value, label) of each item
+ content: none,
+ side: "west"
+ )
+)
+
+#let pyramid-default-style = default-style
+
+/// Draw a pyramid chart
+///
+/// ```cexample
+/// let data = (
+/// "transcendence",
+/// "self-actualization",
+/// "aesthetic",
+/// "cognitive",
+/// "esteem",
+/// "belonging and love",
+/// "safety",
+/// "physiological"
+/// )
+/// let colors = (
+/// rgb("#FFFFC5"), rgb("#FEB6A5"),
+/// rgb("#FFD89F"), rgb("#C6C6C6"),
+/// rgb("#D4D1FF"), rgb("#FFB7CD"),
+/// rgb("#F7BCFF"), rgb("#BDE0B0"),
+/// )
+///
+/// chart.pyramid(
+/// data,
+/// level-style: colors,
+/// level-height: 0.7)
+/// ```
+///
+/// === Styling
+/// *Root* `pyramid` \
+/// #show-parameter-block("level-height", ("number"), [
+/// Minimum level height.], default: 1)
+/// #show-parameter-block("gap", ("number", "ratio"), [
+/// Gap between levels to leave empty. If `mode` is "AREA-HEIGHT", the value must be a ratio and will be proportional to the height of the first level.], default: 0)
+/// #show-parameter-block("mode", ("string"), [
+/// The mode of how to shape each level:
+/// - "REGULAR": All levels have the same height and make a perfectly triangular pyramid
+/// - "AREA-HEIGHT": The area of each level is proportional to its value. Only the height is adapted, keeping the pyramid triangular
+/// - "HEIGHT": The height of each level is proportional to its value. The pyramid is kept as a perfect triangle
+/// - "WIDTH": The height of each level is fixed, but its width is proportional to the value. The pyramid might not be perfectly triangular], default: "REGULAR")
+/// #show-parameter-block("side-label.content", ("none","string","function"), [
+/// Content to display outsides the charts levels, on the side.
+/// There are the following predefined values:
+/// / LABEL: Display the levels 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 levels value
+/// If passed a `` of the format `(value, label) => content`,
+/// that function gets called with each levels value and label and must return
+/// content, that gets displayed.], default: none)
+/// #show-parameter-block("side-label.side", ("string"), [
+/// The side of the chart on which to place side labels, either "west" or "east"], default: "west")
+/// #show-parameter-block("inner-label.content", ("none","string","function"), [
+/// Content to display insides the charts levels.
+/// See `side-label.content` for the possible values.], default: "LABEL")
+/// #show-parameter-block("inner-label.force-inside", ("boolean"), [
+/// If false, labels are automatically placed outside their correspoding levels if they don't fit inside. If true, they are always placed inside.], default: false)
+///
+/// === Anchors
+/// The chart places one anchor per item at the center of its level that
+/// gets named `"levels."`, one on the middle of its left side named `"levels..west"`, and one on the right side named `"levels..east"`,
+/// where index is the index of the level 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 level
+/// - An array: An array which is read depending on value-key and label-key
+/// - A dictionary: A dictionary which is read depending on value-key and label-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.
+/// - level-style (function,array,gradient): Level 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 level the style at the levels
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the the levels
+/// index divided by the number of levels 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 pyramid(
+ data,
+ value-key: none,
+ label-key: none,
+ level-style: palette.red,
+ name: none,
+ ..style
+) = {
+ // Prepare data by converting it to tuples of the format
+ // (value, label)
+ data = data.enumerate().map(((i, item)) => (
+ if value-key != none {
+ item.at(value-key)
+ } else {
+ none
+ },
+ if label-key != none {
+ item.at(label-key)
+ } else {
+ item
+ }
+ ))
+
+ draw.group(name: name, ctx => {
+ draw.anchor("default", (0, 0))
+
+ let style = styles.resolve(
+ ctx.style,
+ merge: style.named(),
+ root: "pyramid",
+ base: default-style
+ )
+
+ let mode = style.mode
+ let gap = style.gap
+
+ assert(mode in ("REGULAR", "AREA-HEIGHT", "HEIGHT", "AREA-WIDTH", "WIDTH"),
+ message: "Mode must be 'REGULAR', 'AREA-HEIGHT', 'HEIGHT', 'AREA-WIDTH' or 'WIDTH', but is: " + str(mode))
+
+
+ if mode == "AREA-HEIGHT" {
+ if gap == 0 {
+ gap = 0%
+ }
+ assert(type(gap) == ratio, message: "When mode is set to 'AREA-HEIGHT', gap must be of type ratio, but is: " + str(type(gap)))
+ }
+
+ assert(style.side-label.side in ("west", "east"),
+ message: "Side label side must either be 'west' or 'east', but is: " + str(style.side-label.side))
+
+ let style-at = if type(level-style) == function {
+ level-style
+ } else if type(level-style) == array {
+ i => {
+ let s = level-style.at(calc.rem(i, level-style.len()))
+ if type(s) == color or type(s) == gradient {
+ (fill: s)
+ } else {
+ s
+ }
+ }
+ } else if type(level-style) == gradient {
+ i => (fill: level-style.sample(i / data.len() * 100%))
+ }
+
+ let total = data.map(d => d.first()).sum()
+ let get-item-label(item, kind) = {
+ let (value, label, ..) = item
+ if kind == label-kind.value {
+ [#value]
+ } else if kind == label-kind.percentage {
+ if total == none {
+ panic("Using percentage label without values")
+ }
+ [#{calc.round(value / total * 100)}%]
+ } else if kind == label-kind.label {
+ label
+ } else if type(kind) == function {
+ (kind)(value, label)
+ }
+ }
+
+ let rect-overlaps-trapezoid(rect, trapezoid) = {
+ let r = rect
+ let t = trapezoid
+ let side-m = (t.y1 - t.y2) / (t.x1 - t.x2)
+ let side-h = t.y1 - side-m * t.x1
+
+ let x-side = (r.y - side-h) / side-m
+
+ return if r.y > t.y1 {
+ r.x < t.x1
+ } else {
+ r.x < x-side
+ }
+ }
+
+ let total-gaps = style.gap * data.len()
+ let total-height = if mode in ("REGULAR", "WIDTH") {
+ style.level-height * data.len()
+ } else if mode == "HEIGHT" {
+
+ }
+
+ total-height += total-gaps
+
+ let base-width = if mode == "REGULAR" {
+ 2 * total-height / calc.sqrt(3)
+
+ }
+
+ let enum-items = data.enumerate()
+
+ // Array of levels
+ // level = (
+ // y,
+ // h,
+ // w-top,
+ // w-bottom,
+ // item
+ // )
+ let levels = ()
+
+ if mode == "REGULAR" {
+ levels = enum-items.map(((i, item)) => (
+ i * (style.level-height + style.gap),
+ style.level-height,
+ auto,
+ auto,
+ item
+ ))
+ } else if mode == "WIDTH" {
+ let w = 0
+ let last-val = 0
+ levels = ()
+ for (i, item) in enum-items {
+ let val = item.first()
+ let y = i * (style.level-height + style.gap)
+ let h = style.level-height
+ let y2 = y + h
+ let w1 = w
+ let w2 = if i == 0 {
+ y2 / calc.sqrt(3)
+ } else {
+ w * val / last-val
+ }
+ w = w2
+ last-val = val
+ levels.push((
+ y,
+ h,
+ w1,
+ w2,
+ item
+ ))
+ }
+ } else if mode == "HEIGHT" {
+ let smallest = calc.min(
+ ..data.map(d => d.first())
+ .filter(v => v != 0)
+ )
+
+ let get-height(value) = {
+ return value / smallest * style.level-height
+ }
+
+ let y = 0
+ for (i, item) in enum-items {
+ let h = get-height(item.first())
+ levels.push((
+ y,
+ h,
+ auto,
+ auto,
+ item
+ ))
+ y += h + style.gap
+ }
+
+ } else if mode == "AREA-HEIGHT" {
+ let y = 0
+ let get-area(y, h) = {
+ return h * (2 *y + h) / 2
+ }
+ let h-for-area(y, area) = {
+ /*
+ A = (2yh + h²)/2
+ 2 * A = 2yh + h²
+ h² + 2yh - 2A = 0
+
+ h = -y +- sqrt(y² + 2A)
+ */
+
+ let delta = calc.sqrt(y * y + 2 * area)
+ return delta - y
+ }
+
+ let first-val = enum-items.first().last().first()
+ let first-area = 1 / calc.sqrt(3)
+ let thinnest = 1
+
+ for (i, item) in enum-items {
+ let h = if i == 0 {
+ 1
+ } else {
+ let area = item.first() / first-val * first-area
+ h-for-area(y, area)
+ }
+
+ levels.push((
+ y,
+ h,
+ auto,
+ auto,
+ item
+ ))
+ thinnest = calc.min(thinnest, h)
+ y += h + gap / 100%
+ }
+
+ let f = style.level-height / thinnest
+
+ levels = levels.map(((y, h, w1, w2, item)) => (
+ y * f,
+ h * f,
+ w1,
+ w2,
+ item
+ ))
+ }
+
+ let anchors = ()
+ draw.group(name: "chart", {
+ draw.group(name: "levels", {
+ for (i, level) in levels.enumerate() {
+ let (
+ y,
+ h,
+ width-top,
+ width-bottom,
+ (value, label)
+ ) = level
+
+ let y2 = y + h
+ if width-top == auto {
+ width-top = y / calc.sqrt(3)
+ }
+ if width-bottom == auto {
+ width-bottom = y2 / calc.sqrt(3)
+ }
+
+ let stroke = style-at(i).at("stroke", default: style.stroke)
+ let fill = style-at(i).at("fill", default: style.fill)
+
+ let lvl-name = str(i)
+ draw.group(name: lvl-name, {
+ draw.line(
+ (-width-top, -y),
+ (width-top, -y),
+ (width-bottom, -y2),
+ (-width-bottom, -y2),
+ close: true,
+ fill: fill,
+ stroke: stroke
+ )
+ let my = -(y + y2)/2
+ draw.anchor(
+ "west",
+ (-(width-top + width-bottom)/2, my)
+ )
+ draw.anchor(
+ "east",
+ ((width-top + width-bottom)/2, my)
+ )
+ draw.anchor("center", (0, my))
+ draw.anchor("default", (0, my))
+ })
+
+ let inner-label = get-item-label(
+ (value, label),
+ style.inner-label.content
+ )
+
+ if inner-label != none {
+ let my = if width-top == 0 {
+ -y - 2*h/3
+ } else {
+ -y - h/2
+ }
+ let m = measure(inner-label)
+ let rw = m.width / ctx.length
+ let rh = m.height / ctx.length
+ let rect = (
+ x: -rw / 2,
+ y: my + rh / 2,
+ w: rw,
+ h: rh
+ )
+ let trapezoid = (
+ y1: -y,
+ y2: -y2,
+ x1: -width-top,
+ x2: -width-bottom
+ )
+
+ let mid = (0, my)
+ if not style.inner-label.force-inside and rect-overlaps-trapezoid(rect, trapezoid) {
+ let (anchor, f) = if calc.rem(i, 2) == 0 {
+ ("west", 1)
+ } else {
+ ("east", -1)
+ }
+
+ let dy = 0.1 * h
+ let pt = (
+ rel: (
+ f * ((-my + dy) / calc.sqrt(3) + .5),
+ dy
+ ),
+ to: mid
+ )
+
+ draw.line(mid, pt)
+ draw.content(
+ pt,
+ inner-label,
+ anchor: anchor,
+ padding: 3pt
+ )
+ } else {
+ draw.content(
+ mid,
+ inner-label
+ )
+ }
+ }
+ }
+ })
+ if style.side-label.content != none {
+ let side = style.side-label.side
+ let corner = "levels.north-" + side
+ for (i, item) in enum-items {
+ let lvl-pt = "levels." + str(i)
+ let pos = (lvl-pt, "-|", corner)
+ draw.content(
+ pos,
+ get-item-label(item, style.side-label.content),
+ anchor: ("west":"east","east":"west").at(side)
+ )
+ }
+ }
+ })
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/chart/radarchart.typ b/packages/preview/cetz-plot/0.1.4/src/chart/radarchart.typ
new file mode 100644
index 0000000000..3dbbdb4ab6
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/chart/radarchart.typ
@@ -0,0 +1,176 @@
+#import "/src/cetz.typ": draw, palette, styles
+
+#import "/src/plot.typ"
+
+#let radarchart-default-style = (
+ web-style: (
+ stroke: black.lighten(40%),
+ ),
+ web-ticks: 4,
+ web-label-offset: 0.4,
+ center-pos: (0, 0),
+ radius: 2,
+)
+
+/// Draw a radar chart (also known as spider chart or web chart). A radar
+/// chart is a chart that represents multivariate data in the form of a
+/// two-dimensional chart of three or more quantitative variables represented as
+/// axes starting from the same point.
+///
+/// ```cexample
+/// chart.radarchart(
+/// (
+/// [A],
+/// [B],
+/// [C],
+/// [D],
+/// [E],
+/// [F],
+/// ),
+/// (0.3, 0.6, 0.3, 0.4, 0.8, 1),
+/// )
+/// ```
+/// === Styling
+/// Can be applied with `cetz.draw.set-style(radarchart: (web-ticks: 6))`.
+///
+/// *Root*: `radarchart`.
+/// #show-parameter-block("web-style", "style", default: (stroke: black.lighten(40%)), [
+/// Style of the web in the background of the chart.])
+/// #show-parameter-block("web-ticks", ("int", "array"), default: 4, [
+/// Amount of layers of the web or an array containing the distance of each web layer to draw.])
+/// #show-parameter-block("web-label-offset", "float", default: 0.4, [
+/// Distance from the end of the web to the label.])
+/// #show-parameter-block("center-pos", "float", default: 1, [
+/// Coordinate of the center of the chart.])
+/// #show-parameter-block("radius", "float", default: 2, [
+/// Radius of the radar chart.])
+///
+/// - labels (array): Array of content. Each entry is the label
+/// of one coordinate axis.
+///
+/// *Example*
+/// ```typc
+/// ([A], [B], [C])
+/// ```
+/// - data (array): Array of data rows. A row can be of type array of float or
+/// array of array of float. All float values must be within the
+/// the range $0 <= "value" <= "radius"$. Each of the data rows must
+/// contain the same amount of items as `labels`.
+///
+/// *Example*
+/// ```typc
+/// ((0.5, 0.3, 0.9), (0.3, 0.5, 0.2))
+/// ```
+/// - data-style (function, array): Style per data row. Can be either
+/// - function: A function of the form `index => style` that must return a style dictionary.
+/// This can be a `palette` function.
+/// - array of style dictionaries: The dictionary at index `i` contains the style for the data row at index `i`.
+/// - array of colors: The dictionary at index `i` contains the fill color for the data row at index `i`.
+///
+#let radarchart(
+ labels,
+ data,
+ data-style: palette.red,
+ ..style,
+) = {
+ assert(type(labels) == array)
+ assert(labels.len() >= 3)
+
+ assert(type(data) == array)
+ assert(data.len() != 0)
+ if type(data.at(0)) != array {
+ // only one single data line
+ data = (data,)
+ }
+
+ // ensure that all data lines have the same amount of coordinates
+ let size = labels.len()
+ for line in data {
+ assert(line.len() == size)
+ }
+
+ draw.group(ctx => {
+ let style = styles.resolve(
+ ctx.style,
+ merge: style.named(),
+ root: "radarchart",
+ base: radarchart-default-style,
+ )
+ draw.set-style(..style)
+
+ let center-pos = style.at("center-pos")
+ let radius = style.at("radius")
+ let web-ticks = style.at("web-ticks")
+ let web-label-offset = style.at("web-label-offset")
+
+ // ensure that no data point overflows out of the chart
+ for line in data {
+ for value in line {
+ assert(0 <= value and value <= radius)
+ }
+ }
+
+ assert(radius > 0)
+ assert(type(web-ticks) in (int, array))
+ if type(web-ticks) == int {
+ // automatically calculate ticks amount of equidistant ticks
+ web-ticks = range(web-ticks).map(i => (i + 1) / web-ticks)
+ }
+
+ let angle-step = 360deg / labels.len()
+
+ // draw labels and lines from center to label
+ // each of these axis is assigned the label "axis-{i}"
+ for (i, label) in labels.enumerate() {
+ let axis-name = "axis-" + str(i)
+ draw.line(
+ center-pos,
+ (
+ rel: (-angle-step * i + 90deg, radius),
+ ),
+ name: axis-name,
+ )
+ draw.content(
+ (axis-name + ".start", radius + web-label-offset, axis-name + ".end"),
+ label,
+ )
+ }
+
+ // web drawing logic
+ for tick in web-ticks {
+ let web-points = ()
+ for i in range(labels.len()) {
+ web-points.push((
+ rel: (-angle-step * i + 90deg, radius * tick),
+ to: center-pos,
+ ))
+ }
+ draw.line(..web-points, close: true, ..style.at("web-style"))
+ }
+
+ // draw the coordinates of each data line as a polygon
+ for (line-index, line) in data.enumerate() {
+ let pts = ()
+ for (i, value) in line.enumerate() {
+ let axis-name = "axis-" + str(i)
+ pts.push((axis-name + ".start", radius * value, axis-name + ".end"))
+ }
+
+ let polygon-style = (:)
+ if type(data-style) == array {
+ let s = data-style.at(line-index)
+ if type(data-style.at(line-index)) == dictionary {
+ // data-style = style dict
+ polygon-style = s
+ } else {
+ // data-style = list of colors -> fill polygon with these colors
+ polygon-style = (fill: s)
+ }
+ } else if type(data-style) == function {
+ // data-style = method taking the index as param
+ polygon-style = data-style(line-index)
+ }
+ draw.line(..pts, close: true, ..polygon-style)
+ }
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/clip.typ b/packages/preview/cetz-plot/0.1.4/src/clip.typ
new file mode 100644
index 0000000000..b4eac3b1f3
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/clip.typ
@@ -0,0 +1,65 @@
+#import "/src/cetz.typ": vector
+
+/// Clip a single component of a list of vectors between [min, max]
+/// and returns a list of clipped shapes.
+#let clip-component-axis(component, min, max, points) = {
+ if points == () {
+ return ()
+ }
+
+ // List of tuples (index, ratio, inside)
+ let intersections = ()
+
+ // Collect all intersections
+ let inside = auto
+ for (i, pt) in points.enumerate() {
+ let v = pt.at(component, default: min)
+ let this-inside = min <= v and v <= max
+
+ if inside == auto {
+ inside = this-inside
+ } else if inside != this-inside {
+ inside = not inside
+ if this-inside {
+ intersections.push((i, (v - min) / (max - min), inside))
+ } else {
+ intersections.push((i, (v - min) / (max - min), inside))
+ }
+ }
+ }
+
+ // Clip intersections
+ if intersections != () {
+ let shapes = ()
+
+ let start = none
+ let start-ratio = none
+ for (i, ratio, this-inside) in intersections {
+ if this-inside {
+ start = i
+ start-ratio = ratio
+ } else {
+ let start-pt = if start == 0 or start == points.len() - 1 {
+ points.at(start)
+ } else {
+ vector.lerp(points.at(start - 1), points.at(start), start-ratio)
+ }
+
+ let end-pt = if i == points.len() - 1 {
+ points.last()
+ } else {
+ vector.lerp(points.at(i - 1), points.at(i), ratio)
+ }
+
+ shapes.push((
+ start-pt, ..points.slice(start, i), end-pt,
+ ))
+ start = none
+ }
+ }
+
+ return shapes
+ }
+
+ return (points,)
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/coordinates.typ b/packages/preview/cetz-plot/0.1.4/src/coordinates.typ
new file mode 100644
index 0000000000..c4734df0a7
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/coordinates.typ
@@ -0,0 +1,25 @@
+#import "/src/cetz.typ": draw
+
+/// Coordinate resolve that just forwards (plot: (x, y)) coordinates
+/// as (x, y).
+#let stup-resolver(ctx, point) = {
+ if type(point) == dictionary and "plot" in point {
+ return point.plot
+ }
+
+ return point
+}
+
+/// Plot axis coordinate resolver
+let default-resolver(ctx, point) = {
+ if type(point) == dictionary and "plot" in point {
+ let names = point.at("axes", default: ("x", "y"))
+
+ let p = point.plot
+ return names.map(name => ctx.cetz-plot-axes.at(name)).enumerate().map(((i, ax)) => {
+ (ax.to)(p.at(i))
+ })
+ }
+
+ return c
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/grid-layout.typ b/packages/preview/cetz-plot/0.1.4/src/grid-layout.typ
new file mode 100644
index 0000000000..4e9f05d586
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/grid-layout.typ
@@ -0,0 +1,43 @@
+#import "/src/cetz.typ"
+
+#let grid(columns: 2, ..items) = {
+ let items = items.pos()
+ let valign = (horizon,) * columns
+
+ (ctx => {
+ let rows = ()
+ let cells = ()
+
+ for (i, item) in items.enumerate() {
+ let (bounds: bounds, drawables: drawables, ..) = cetz.process.many(ctx, item)
+
+ let width = bounds.high.at(0) - bounds.low.at(0)
+ let height = bounds.high.at(1) - bounds.low.at(1)
+ cells.push(((width, height), drawables))
+ }
+
+ let height = 0
+ for i in range(0, items.len()) {
+ let size = cells.at(i).at(0)
+ height = calc.max(size.at(1), height)
+ if calc.rem(i, columns) == 0{
+ rows.push(height)
+ height = 0
+ }
+ }
+
+ cells = cells.enumerate().map(((i, (size, cell))) => {
+ let t = cetz.matrix.ident()
+ let row = rows.at(int(i / columns))
+
+ t = cetz.matrix.transform-translate(0, -(row - size.at(1)) / 2, 0)
+
+ cetz.drawable.apply-transform(t, cell)
+ })
+
+ return (
+ ctx: ctx,
+ drawables: cells.join(),
+ )
+ },)
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/lib.typ b/packages/preview/cetz-plot/0.1.4/src/lib.typ
new file mode 100644
index 0000000000..9a1f4c1bc6
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/lib.typ
@@ -0,0 +1,6 @@
+#let version = version(0,1,4)
+
+#import "/src/axes.typ"
+#import "/src/plot.typ"
+#import "/src/chart.typ"
+#import "/src/smartart.typ"
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot.typ b/packages/preview/cetz-plot/0.1.4/src/plot.typ
new file mode 100644
index 0000000000..f0f55cae6c
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot.typ
@@ -0,0 +1,567 @@
+#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)
+
+#let default-plot-style(i) = {
+ let color = default-colors.at(calc.rem(i, default-colors.len()))
+ return (stroke: color,
+ fill: color.lighten(75%))
+}
+
+#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:`.
+///
+/// ```cexample
+/// 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.
+///
+/// === 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.
+/// ```cexample
+/// 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.
+///
+/// ```cexample
+/// 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
+///
+/// ```cexample
+/// 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("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.
+///
+/// ```cexample
+/// 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).
+///
+/// ```cexample-vertical
+/// let opts = (x-tick-step: none, y-tick-step: none, size: (2,1))
+/// let data = 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))
+/// }
+/// ```
+/// - 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 => {
+ draw.assert-version(version(0, 5, 0), max: version(0, 6, 0))
+
+ // 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")
+
+ 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 (x: x, y: y, size: size, x-scale: x-scale, y-scale: y-scale)
+ }
+
+ // 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 => {
+ let ((x0, x1, x2, x3),
+ (y0, y1, y2, y3),
+ (z0, z1, z2, z3),
+ (w0, w1, w2, w3)) = ctx.transform
+ ctx.transform = ((x1, x0, x2, x3),
+ (y1, y0, y2, y3),
+ (z1, z0, z2, z3),
+ (w1, w0, w2, w3))
+ return ctx
+ }) + body
+ }
+
+ // Setup the viewport
+ axes.axis-viewport(size, x, y, none, body, name: name)
+ }
+
+ 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) }
+ }
+
+ assert(axis-style in (none, "scientific", "scientific-auto", "school-book", "left"),
+ message: "Invalid plot style")
+
+ // Create axes for data & annotations
+ let axis-dict = (:)
+ for d in data + annotations {
+ if "axes" not in d { continue }
+
+ for (i, name) in d.axes.enumerate() {
+ if not name in axis-dict {
+ axis-dict.insert(name, axes.axis(
+ min: none, max: none))
+ }
+
+ let axis = axis-dict.at(name)
+ let domain = if i == 0 {
+ d.at("x-domain", default: (none, none))
+ } else {
+ d.at("y-domain", default: (none, none))
+ }
+ if domain != (none, none) {
+ axis.min = util.min(axis.min, ..domain)
+ axis.max = util.max(axis.max, ..domain)
+ }
+
+ axis-dict.at(name) = axis
+ }
+ }
+
+ // Create axes for anchors
+ for a in anchors {
+ for (i, name) in a.axes.enumerate() {
+ if not name in axis-dict {
+ axis-dict.insert(name, axes.axis(min: none, max: none))
+ }
+ }
+ }
+
+ // Adjust axis bounds for annotations
+ for a in annotations {
+ let (x, y) = a.axes.map(name => axis-dict.at(name))
+ (x, y) = calc-annotation-domain(ctx, x, y, a)
+ axis-dict.at(a.axes.at(0)) = x
+ axis-dict.at(a.axes.at(1)) = y
+ }
+
+ // Set axis options
+ axis-dict = plot-util.setup-axes(ctx, axis-dict, options.named(), size)
+
+ // Prepare styles
+ for i in range(data.len()) {
+ if "style" not in data.at(i) { continue }
+
+ let style-base = plot-style
+ if type(style-base) == function {
+ style-base = (style-base)(i)
+ }
+ assert.eq(type(style-base), dictionary,
+ message: "plot-style must be of type dictionary")
+
+ if type(data.at(i).style) == function {
+ data.at(i).style = (data.at(i).style)(i)
+ }
+ assert.eq(type(style-base), dictionary,
+ message: "data plot-style must be of type dictionary")
+
+ data.at(i).style = util.merge-dictionary(
+ style-base, data.at(i).style)
+
+ if "mark-style" in data.at(i) {
+ let mark-style-base = mark-style
+ if type(mark-style-base) == function {
+ mark-style-base = (mark-style-base)(i)
+ }
+ assert.eq(type(mark-style-base), dictionary,
+ message: "mark-style must be of type dictionary")
+
+ if type(data.at(i).mark-style) == function {
+ data.at(i).mark-style = (data.at(i).mark-style)(i)
+ }
+
+ if type(data.at(i).mark-style) == dictionary {
+ data.at(i).mark-style = util.merge-dictionary(
+ mark-style-base,
+ data.at(i).mark-style
+ )
+ }
+ }
+ }
+
+ draw.group(name: "plot", {
+ draw.anchor("origin", (0, 0))
+
+ // Prepare
+ for i in range(data.len()) {
+ if "axes" not in data.at(i) { continue }
+
+ let (x, y) = data.at(i).axes.map(name => axis-dict.at(name))
+ let plot-ctx = make-ctx(x, y, size)
+
+ if "plot-prepare" in data.at(i) {
+ data.at(i) = (data.at(i).plot-prepare)(data.at(i), plot-ctx)
+ assert(data.at(i) != none,
+ message: "Plot prepare(self, cxt) returned none!")
+ }
+ }
+
+ // Background Annotations
+ for a in annotations.filter(a => a.background) {
+ let (x, y) = a.axes.map(name => axis-dict.at(name))
+ let plot-ctx = make-ctx(x, y, size)
+
+ data-viewport(a, x, y, size, {
+ draw.anchor("default", (0, 0))
+ a.body
+ })
+ }
+
+ // Fill
+ if fill-below {
+ 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)
+
+ data-viewport(d, x, y, size, {
+ draw.anchor("default", (0, 0))
+ draw.set-style(..d.style)
+
+ 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,)
+ }
+
+ // 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)
+
+ data-viewport(d, x, y, 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)
+ }
+ })
+
+ if "mark" in d and d.mark != none {
+ draw.scope({
+ if y.horizontal {
+ draw.set-ctx(ctx => {
+ let ((x0, x1, x2, x3),
+ (y0, y1, y2, y3),
+ (z0, z1, z2, z3),
+ (w0, w1, w2, w3)) = ctx.transform
+ ctx.transform = ((x1, x0, x2, x3),
+ (y1, y0, y2, y3),
+ (z1, z0, z2, z3),
+ (w1, w0, w2, w3))
+ return ctx
+ })
+ }
+
+ draw.set-style(..d.style, ..d.mark-style)
+ mark.draw-mark(d.data, x, y, 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)
+
+ data-viewport(a, x, y, size, {
+ draw.anchor("default", (0, 0))
+ a.body
+ })
+ }
+
+ // 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 pt = a.position.enumerate().map(((i, v)) => {
+ if v == "min" { return axis-dict.at(a.axes.at(i)).min }
+ if v == "max" { return axis-dict.at(a.axes.at(i)).max }
+ return v
+ })
+ pt = axes.transform-vec(size, x, y, none, pt)
+ if pt != none {
+ draw.anchor(a.name, pt)
+ }
+ }
+ })
+
+ // Draw the legend
+ if legend != none {
+ let items = data.filter(d => "label" in d and d.label != none)
+ if items.len() > 0 {
+ let legend-style = styles.resolve(ctx.style,
+ base: plot-legend.default-style, merge: legend-style, root: "legend")
+
+ plot-legend.add-legend-anchors(legend-style, "plot", size)
+ plot-legend.legend(legend, anchor: legend-anchor, {
+ for item in items {
+ let preview = if "plot-legend-preview" in item {
+ _ => {(item.plot-legend-preview)(item) }
+ } else {
+ auto
+ }
+
+ plot-legend.item(item.label, preview,
+ mark: item.at("mark", default: none),
+ mark-size: item.at("mark-size", default: none),
+ mark-style: item.at("mark-style", default: none),
+ ..item.at("style", default: (:)))
+ }
+ }, ..legend-style)
+ }
+ }
+
+ 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.
+///
+/// ```cexample
+/// 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,
+ ),)
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/annotation.typ b/packages/preview/cetz-plot/0.1.4/src/plot/annotation.typ
new file mode 100644
index 0000000000..d4d4479f97
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/annotation.typ
@@ -0,0 +1,75 @@
+#import "/src/cetz.typ"
+#import cetz: draw, process, util, matrix
+#import "util.typ"
+#import "sample.typ"
+
+/// Add an annotation to the plot
+///
+/// An annotation is a sub-canvas that uses the plots coordinates specified
+/// by its x and y axis.
+///
+/// ```cexample
+/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, {
+/// plot.add(domain: (0, 2*calc.pi), calc.sin)
+/// plot.annotate({
+/// rect((0, -1), (calc.pi, 1), fill: rgb(50,50,200,50))
+/// content((calc.pi, 0), [Here])
+/// })
+/// })
+/// ```
+///
+/// Bounds calculation is done naively, therefore fixed size content _can_ grow
+/// out of the plot. You can adjust the padding manually to adjust for that. The
+/// feature of solving the correct bounds for fixed size elements might be added
+/// in the future.
+///
+/// - body (drawable): Elements to draw
+/// - axes (axes): X and Y axis names
+/// - resize (bool): If true, the plots axes get adjusted to contain the annotation
+/// - padding (none,number,dictionary): Annotation padding that is used for axis
+/// 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) = {
+ ((
+ type: "annotation",
+ body: {
+ draw.set-style(mark: (transform-shape: false))
+ body;
+ },
+ axes: axes,
+ resize: resize,
+ background: background,
+ padding: cetz.util.as-padding-dict(padding),
+ ),)
+}
+
+// Returns the adjusted axes for the annotation object
+//
+// -> array Tuple of x and y axis
+#let calc-annotation-domain(ctx, x, y, annotation) = {
+ if not annotation.resize {
+ return (x, y)
+ }
+
+ ctx.transform = matrix.ident(4)
+ let (ctx: ctx, bounds: bounds, drawables: _) = process.many(ctx, annotation.body)
+ if bounds == none {
+ return (x, y)
+ }
+
+ let (x-min, y-min, ..) = bounds.low
+ let (x-max, y-max, ..) = bounds.high
+
+ x-min -= annotation.padding.left
+ x-max += annotation.padding.right
+ y-min -= annotation.padding.bottom
+ y-max += annotation.padding.top
+
+ x.min = calc.min(x.min, x-min)
+ x.max = calc.max(x.max, x-max)
+ y.min = calc.min(y.min, y-min)
+ y.max = calc.max(y.max, y-max)
+
+ return (x, y)
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/bar.typ b/packages/preview/cetz-plot/0.1.4/src/plot/bar.typ
new file mode 100644
index 0000000000..f817f38c52
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/bar.typ
@@ -0,0 +1,264 @@
+#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 retrieving 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 retrieving 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,array): Key(s) to use for retrieving 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/packages/preview/cetz-plot/0.1.4/src/plot/boxwhisker.typ b/packages/preview/cetz-plot/0.1.4/src/plot/boxwhisker.typ
new file mode 100644
index 0000000000..fcdb91b16e
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/boxwhisker.typ
@@ -0,0 +1,118 @@
+#import "/src/cetz.typ": draw, util
+
+/// Add one or more box or whisker plots
+///
+/// ```cexample
+/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, {
+/// 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)),
+ axes: axes,
+ mark: mark,
+ mark-size: mark-size,
+ mark-style: (:)
+ ) }),)
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/contour.typ b/packages/preview/cetz-plot/0.1.4/src/plot/contour.typ
new file mode 100644
index 0000000000..94b6b56ec3
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/contour.typ
@@ -0,0 +1,370 @@
+#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
+// - connect-domain (bool): Treat the sample domain boundary as an exterior edge.
+// This closes regions against the plot boundary, which is useful for fills but
+// not for contour 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,
+ connect-domain: false,
+ 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 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)
+ }
+
+ let segments = ()
+ let x-range = if connect-domain { range(-1, n-cols) } else { range(0, n-cols - 1) }
+ let y-range = if connect-domain { range(-1, n-rows) } else { range(0, n-rows - 1) }
+
+ // 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
+ }
+
+ for y in y-range {
+ for x in x-range {
+ 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
+}
+
+// Convert contour data points from sample-space to plot coordinates.
+#let _scale-contours(contours, x-min, y-min, dx, dy, z) = {
+ contours.map(contour => (
+ z: z,
+ line-data: contour.map(pt => (
+ pt.at(0) * dx + x-min,
+ pt.at(1) * dy + y-min,
+ )),
+ ))
+}
+
+// Prepare line data
+#let _prepare(self, ctx) = {
+ let (x, y) = (ctx.x, ctx.y)
+
+ self.stroke-contours = self.stroke-contours.map(c => {
+ c.stroke-paths = util.compute-stroke-paths(c.line-data, x, y)
+ return c
+ })
+
+ if self.fill {
+ self.fill-contours = self.fill-contours.map(c => {
+ c.fill-paths = util.compute-fill-paths(c.line-data, x, y)
+ return c
+ })
+ }
+
+ return self
+}
+
+// Stroke line data
+#let _stroke(self, ctx) = {
+ for c in self.stroke-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.fill-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.
+///
+/// ```cexample
+/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, {
+/// 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 stroke-contours = ()
+ let fill-contours = ()
+ let z = if type(z) == array { z } else { (z,) }
+ for z in z {
+ stroke-contours += _scale-contours(
+ find-contours(data, z, op: op, interpolate: interpolate, contour-limit: limit),
+ x-min, y-min, dx, dy, z)
+
+ if fill {
+ fill-contours += _scale-contours(
+ find-contours(data, z, op: op, interpolate: interpolate, connect-domain: true, contour-limit: limit),
+ x-min, y-min, dx, dy, z)
+ }
+ }
+
+ return ((
+ type: "contour",
+ label: label,
+ stroke-contours: stroke-contours,
+ fill-contours: fill-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/packages/preview/cetz-plot/0.1.4/src/plot/errorbar.typ b/packages/preview/cetz-plot/0.1.4/src/plot/errorbar.typ
new file mode 100644
index 0000000000..88dc8faa0d
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/errorbar.typ
@@ -0,0 +1,118 @@
+#import "/src/cetz.typ": draw, util, vector
+
+#let _draw-whisker(pt, dir, ..style) = {
+ let a = vector.add(pt, vector.scale(dir, -1))
+ let b = vector.add(pt, vector.scale(dir, +1))
+
+ draw.line(a, b, ..style)
+}
+
+#let draw-errorbar(pt, x, y, x-whisker-size, y-whisker-size, style) = {
+ if type(x) != array { x = (-x, x) }
+ if type(y) != array { y = (-y, y) }
+
+ let (x-min, x-max) = x
+ let x-min-pt = vector.add(pt, (x-min, 0))
+ let x-max-pt = vector.add(pt, (x-max, 0))
+ if x-min != 0 or x-max != 0 {
+ draw.line(x-min-pt, x-max-pt, ..style)
+ if x-whisker-size > 0 {
+ if x-min != 0 {
+ _draw-whisker(x-min-pt, (0, x-whisker-size), ..style)
+ }
+ if x-max != 0 {
+ _draw-whisker(x-max-pt, (0, x-whisker-size), ..style)
+ }
+ }
+ }
+
+ let (y-min, y-max) = y
+ let y-min-pt = vector.add(pt, (0, y-min))
+ let y-max-pt = vector.add(pt, (0, y-max))
+ if y-min != 0 or y-max != 0 {
+ draw.line(y-min-pt, y-max-pt, ..style)
+ if y-whisker-size > 0 {
+ if y-min != 0 {
+ _draw-whisker(y-min-pt, (y-whisker-size, 0), ..style)
+ }
+ if y-max != 0 {
+ _draw-whisker(y-max-pt, (y-whisker-size, 0), ..style)
+ }
+ }
+ }
+}
+
+#let _prepare(self, ctx) = {
+ return self
+}
+
+#let _stroke(self, ctx) = {
+ 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)
+}
+
+/// Add x- and/or y-error bars
+///
+/// - pt (tuple): Error-bar center coordinate tuple: `(x, y)`
+/// - x-error (float,tuple): Single error or tuple of errors along the x-axis
+/// - y-error (float,tuple): Single error or tuple of errors along the y-axis
+/// - mark (none,string): Mark symbol to show at the error position (`pt`).
+/// - mark-size (number): Size of the mark symbol.
+/// - mark-style (style): Extra style to apply to the mark symbol.
+/// - whisker-size (float): Width of the error bar whiskers in canvas units.
+/// - 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)
+ }
+
+ 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 = x-error.map(v => v + x)
+ let y-domain = y-error.map(v => v + y)
+
+ return ((
+ type: "errorbar",
+ label: label,
+ axes: axes,
+ data: ((x,y),),
+ x: x,
+ y: y,
+ x-error: x-error,
+ y-error: y-error,
+ 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,
+ ),)
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/formats.typ b/packages/preview/cetz-plot/0.1.4/src/plot/formats.typ
new file mode 100644
index 0000000000..bb43902597
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/formats.typ
@@ -0,0 +1,164 @@
+// Compare two floats
+#let _compare(a, b, eps: 1e-6) = {
+ return calc.abs(a - b) <= eps
+}
+
+// Pre-computed table of fractions
+#let _common-denoms = range(2, 11 + 1).map(d => {
+ (d, range(1, d).map(n => n/d))
+})
+
+#let _find-fraction(v, denom: auto, eps: 1e-6) = {
+ let i = calc.floor(v)
+ let f = v - i
+ if _compare(f, 0, eps: eps) {
+ return $#v$
+ }
+
+ let denom = if denom != auto {
+ for n in range(1, denom) {
+ if _compare(f, n/denom, eps: eps) {
+ denom
+ }
+ }
+ } else {
+ (() => {
+ for ((denom, tab)) in _common-denoms {
+ for vv in tab {
+ if _compare(f, vv, eps: eps) {
+ return denom
+ }
+ }
+ }
+ })()
+ }
+
+ if denom != none {
+ return if v < 0 { $-$ } else {} + $#calc.round(calc.abs(v) * denom)/#denom$
+ }
+}
+
+/// Fraction tick formatter
+///
+/// ```cexample
+/// plot.plot(size: (5,1),
+/// x-format: plot.formats.fraction,
+/// x-tick-step: 1/5,
+/// y-tick-step: none, {
+/// plot.add(calc.sin, domain: (-1, 1))
+/// })
+/// ```
+///
+/// - value (number): Value to format
+/// - denom (auto, int): Denominator for result fractions. If set to `auto`,
+/// a hardcoded fraction table is used for finding fractions with a
+/// denominator <= 11.
+/// - eps (number): Epsilon used for comparison
+/// -> Content if a matching fraction could be found or none
+#let fraction(value, denom: auto, eps: 1e-6) = {
+ return _find-fraction(value, denom: denom, eps: eps)
+}
+
+/// Multiple of tick formatter
+///
+/// ```cexample
+/// plot.plot(size: (5,1),
+/// x-format: plot.formats.multiple-of,
+/// x-tick-step: calc.pi/4,
+/// y-tick-step: none, {
+/// plot.add(calc.sin, domain: (-calc.pi, 1.5 * calc.pi))
+/// })
+/// ```
+///
+/// - value (number): Value to format
+/// - factor (number): Factor value is expected to be a multiple of.
+/// - symbol (content): Suffix symbol. For `value` = 0, the symbol is not
+/// appended.
+/// - fraction (none, true, int): If not none, try finding matching fractions
+/// using the same mechanism as `fraction`. If set to an integer, that integer
+/// is used as denominator. If set to `none` or `false`, or if no fraction
+/// could be found, a real number with `digits` digits is used.
+/// - digits (int): Number of digits to use for rounding
+/// - eps (number): Epsilon used for comparison
+/// - prefix (content): Content to prefix
+/// - suffix (content): Content to append
+/// -> Content if a matching fraction could be found or none
+#let multiple-of(value, factor: calc.pi, symbol: $pi$, fraction: true, digits: 2, eps: 1e-6, prefix: [], suffix: []) = {
+ if _compare(value, 0, eps: eps) {
+ return $0$
+ }
+
+ let a = value / factor
+ if _compare(a, 1, eps: eps) {
+ return prefix + symbol + suffix
+ } else if _compare(a, -1, eps: eps) {
+ return prefix + $-$ + symbol + suffix
+ }
+
+ if fraction != none {
+ let frac = _find-fraction(a, denom: if fraction == true { auto } else { fraction })
+ if frac != none {
+ return prefix + frac + symbol + suffix
+ }
+ }
+
+ return prefix + $#calc.round(a, digits: digits)$ + symbol + suffix
+}
+
+/// Scientific notation tick formatter
+///
+/// ```cexample
+/// plot.plot(size: (5,1),
+/// x-format: plot.formats.sci,
+/// x-tick-step: 1e3,
+/// y-tick-step: none, {
+/// plot.add(x => x, domain: (-2e3, 2e3))
+/// })
+/// ```
+///
+/// - value (number): Value to format
+/// - digits (int): Number of digits for rounding the factor
+/// - prefix (content): Content to prefix
+/// - suffix (content): Content to append
+/// -> Content
+#let sci(value, digits: 2, prefix: [], suffix: []) = {
+ let exponent = if value != 0 {
+ calc.floor(calc.log(calc.abs(value), base: 10))
+ } else {
+ 0
+ }
+
+ let ee = calc.pow(10.0, calc.abs(exponent + 1))
+ if exponent > 0 {
+ value = value / ee * 10
+ } else if exponent < 0 {
+ value = value * ee * 10
+ }
+
+ value = calc.round(value, digits: digits)
+ if exponent <= -1 or exponent >= 1 {
+ return prefix + $#value times 10^#exponent$ + suffix
+ }
+
+ return prefix + $#value$ + suffix
+}
+
+/// Rounded decimal number formatter
+///
+/// ```cexample
+/// plot.plot(size: (5,1),
+/// x-format: plot.formats.decimal,
+/// x-tick-step: .5,
+/// y-tick-step: none, {
+/// plot.add(x => x, domain: (-1, 1))
+/// })
+/// ```
+///
+/// - value (number): Value to format
+/// - digits (int): Number of digits to round to
+/// - prefix (content): Content to prefix
+/// - suffix (content): Content to append
+/// -> Content
+#let decimal(value, digits: 2, prefix: [], suffix: []) = {
+ prefix + $#calc.round(value, digits: digits)$ + suffix
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/legend.typ b/packages/preview/cetz-plot/0.1.4/src/plot/legend.typ
new file mode 100644
index 0000000000..94838d4a55
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/legend.typ
@@ -0,0 +1,242 @@
+#import "/src/cetz.typ"
+#import cetz: draw, styles
+#import draw: group
+
+#import "mark.typ": draw-mark-shape
+
+#let default-style = (
+ orientation: ttb,
+ default-position: "north-east",
+ layer: 1, // Legend layer
+ fill: rgb(255,255,255,200), // Legend background
+ stroke: black, // Legend border
+ padding: .1, // Legend border padding
+ offset: (0, 0), // Legend displacement
+ spacing: .1, // Spacing between anchor and legend
+ item: (
+ radius: 0,
+ spacing: 0, // Extra spacing between items
+ preview: (
+ width: .75, // Preview width
+ height: .3, // Preview height
+ margin: .1 // Distance between preview and label
+ )
+ ),
+ radius: 0,
+ scale: 100%,
+)
+
+// Map position to legend group anchor
+#let auto-group-anchor = (
+ inner-north-west: "north-west",
+ inner-north: "north",
+ inner-north-east: "north-east",
+ inner-south-west: "south-west",
+ inner-south: "south",
+ inner-south-east: "south-east",
+ inner-west: "west",
+ inner-east: "east",
+ north-west: "north-east",
+ north: "south",
+ north-east: "north-west",
+ south-west: "south-east",
+ south: "north",
+ south-east: "south-west",
+ east: "west",
+ west: "east",
+)
+
+// Generate legend positioning anchors
+#let add-legend-anchors(style, element, size) = {
+ import draw: *
+ let (w, h) = size
+ let (xo, yo) = {
+ let spacing = style.at("spacing", default: (0, 0))
+ if type(spacing) == array {
+ spacing
+ } else {
+ (spacing, spacing)
+ }
+ }
+
+ anchor("north", (rel: (w / 2, yo), to: (element + ".north", "-|", element + ".origin")))
+ anchor("south", (rel: (w / 2, -yo), to: (element + ".south", "-|", element + ".origin")))
+ anchor("east", (rel: (xo, h / 2), to: (element + ".east", "|-", element + ".origin")))
+ anchor("west", (rel: (-xo, h / 2), to: (element + ".west", "|-", element + ".origin")))
+ anchor("north-east", (rel: (xo, h), to: (element + ".north-east", "|-", element + ".origin")))
+ anchor("north-west", (rel: (-xo, h), to: (element + ".north-west", "|-", element + ".origin")))
+ anchor("south-east", (rel: (xo, 0), to: (element + ".south-east", "|-", element + ".origin")))
+ anchor("south-west", (rel: (-xo, 0), to: (element + ".south-west", "|-", element + ".origin")))
+ anchor("inner-north", (rel: (w / 2, h - yo), to: element + ".origin"))
+ anchor("inner-north-east", (rel: (w - xo, h - yo), to: element + ".origin"))
+ anchor("inner-north-west", (rel: (yo, h - yo), to: element + ".origin"))
+ anchor("inner-south", (rel: (w / 2, yo), to: element + ".origin"))
+ anchor("inner-south-east", (rel: (w - xo, yo), to: element + ".origin"))
+ anchor("inner-south-west", (rel: (xo, yo), to: element + ".origin"))
+ anchor("inner-east", (rel: (w - xo, h / 2), to: element + ".origin"))
+ anchor("inner-west", (rel: (xo, h / 2), to: element + ".origin"))
+}
+
+// Draw a generic item preview
+#let draw-generic-preview(item) = {
+ import draw: *
+
+ if item.at("fill", default: false) {
+ rect((0,0), (1,1), ..item.style)
+ } else {
+ line((0,.5), (1,.5), ..item.style)
+ }
+}
+
+/// Construct a legend item for use with the `legend` function
+///
+/// - label (none, auto, content): Legend label or auto to use the enumerated default label
+/// - preview (auto, function): Legend preview icon function of the format `item => elements`.
+/// Note that the canvas bounds for drawing the preview are (0,0) to (1,1).
+/// - mark (none,string): Legend mark symbol
+/// - mark-style (none,dictionary): Mark style
+/// - mark-size (number): Mark size
+/// - ..style (styles): Style keys for the single item
+#let item(label, preview, mark: none, mark-style: (:), mark-size: 1, ..style) = {
+ assert.eq(style.pos().len(), 0,
+ message: "Unexpected positional arguments")
+ return ((label: label, preview: preview,
+ mark: mark, mark-style: mark-style, mark-size: mark-size,
+ style: style.named()),)
+}
+
+/// Draw a legend
+#let legend(position, items, name: "legend", ..style) = group(name: name, ctx => {
+ draw.anchor("default", ())
+ let items = if items != none { items.filter(v => v.label != none) } else { () }
+ if items == () {
+ return
+ }
+
+ let style = styles.resolve(
+ ctx.style, merge: style.named(), base: default-style, root: "legend")
+ assert(style.orientation in (ttb, ltr),
+ message: "Unsupported legend orientation.")
+
+ // Scaling
+ draw.scale(style.scale)
+
+ // Position
+ let position = if position == auto {
+ style.default-position
+ } else {
+ position
+ }
+
+ // Adjust anchor
+ if style.anchor == auto {
+ style.anchor = if type(position) == str {
+ auto-group-anchor.at(position, default: "north-west")
+ } else {
+ "north-west"
+ }
+ }
+
+ // Apply offset
+ if style.offset not in (none, (0,0)) {
+ position = (rel: style.offset, to: position)
+ }
+
+ // Draw items
+ draw.on-layer(style.layer, {
+ draw.group(name: "items", padding: style.padding, ctx => {
+ import draw: *
+
+ set-origin(position)
+ anchor("default", (0,0))
+
+ let pt = (0, 0)
+ for (i, item) in items.enumerate() {
+ let (label, preview) = item
+ if label == none {
+ continue
+ } else if label == auto {
+ label = $ f_(#i) $
+ }
+
+ group({
+ anchor("default", (0,0))
+
+ let row-height = style.item.preview.height
+ let preview-width = style.item.preview.width
+ let preview-a = (0, -row-height / 2)
+ let preview-b = (preview-width, +row-height / 2)
+ let label-west = (preview-width + style.item.preview.margin, 0)
+
+ // Draw item preview
+ let draw-preview = if preview == auto { draw-generic-preview } else { preview }
+ group({ // BUG: scope in group seems to be bugged, we use group instead
+ set-viewport(preview-a, preview-b, bounds: (1, 1, 0))
+ (draw-preview)(item)
+ })
+
+ // Draw mark preview
+ let mark = item.at("mark", default: none)
+ if mark != none {
+ draw-mark-shape((preview-a, 50%, preview-b),
+ calc.min(style.item.preview.width / 2, item.mark-size),
+ mark,
+ item.mark-style)
+ }
+
+ // Draw label
+ content(label-west,
+ text(top-edge: "ascender", bottom-edge: "descender", align(left + horizon, label)),
+ name: "label", anchor: "west")
+ }, name: "item", anchor: if style.orientation == ltr { "west" } else { "north-west" })
+
+ if style.orientation == ttb {
+ set-origin((rel: (0, -style.item.spacing),
+ to: "item.south-west"))
+ } else if style.orientation == ltr {
+ set-origin((rel: (style.item.spacing, 0),
+ to: "item.east"))
+ }
+ }
+ }, anchor: style.anchor)
+ })
+
+ // Fill legend background
+ draw.on-layer(style.layer - .5, {
+ draw.rect("items.south-west",
+ "items.north-east", fill: style.fill, stroke: style.stroke, radius: style.radius)
+ })
+})
+
+/// Function for manually adding a legend item from within
+/// a plot environment
+///
+/// - label (content): Legend label
+/// - preview (auto,function): Legend preview function of the format `() => elements`.
+/// The preview canvas bounds are between (0,0) and (1,1).
+/// If set to `auto`, a straight line is drawn.
+///
+/// ```cexample
+/// plot.plot(size: (1,1), x-tick-step: none, y-tick-step: none, {
+/// plot.add(((0,0), (1,1))) // Some data
+/// plot.add-legend([Custom item], preview: () => {
+/// import cetz.draw: *
+/// circle((.5,.5), radius: .5) // Draw a custom preview
+/// // between (0,0) and (1,1)
+/// })
+/// plot.add-legend([Another item])
+/// })
+/// ```
+#let add-legend(label, preview: auto) = {
+ assert(preview == auto or type(preview) == function,
+ message: "Expected auto or function, got " + repr(type(preview)))
+
+ return ((
+ type: "legend-item",
+ label: label,
+ style: (:),
+ axes: ("x", "y"),
+ ) + if preview != auto {
+ (plot-legend-preview: _ => { preview() })
+ },)
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/line.typ b/packages/preview/cetz-plot/0.1.4/src/plot/line.typ
new file mode 100644
index 0000000000..aa89623ab9
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/line.typ
@@ -0,0 +1,527 @@
+#import "/src/cetz.typ": draw
+
+#import "util.typ"
+#import "sample.typ"
+
+// Transform points
+//
+// - data (array): Data points
+// - line (str,dictionary): Line line
+#let transform-lines(data, line) = {
+ let hvh-data(t) = {
+ if type(t) == ratio {
+ t = t / 1%
+ }
+ t = calc.max(0, calc.min(t, 1))
+
+ let pts = ()
+
+ let len = data.len()
+ for i in range(0, len) {
+ pts.push(data.at(i))
+
+ if i < len - 1 {
+ let (a, b) = (data.at(i), data.at(i+1))
+ if t == 0 {
+ pts.push((a.at(0), b.at(1)))
+ } else if t == 1 {
+ pts.push((b.at(0), a.at(1)))
+ } else {
+ let x = a.at(0) + (b.at(0) - a.at(0)) * t
+ pts.push((x, a.at(1)))
+ pts.push((x, b.at(1)))
+ }
+ }
+ }
+ return pts
+ }
+
+ if type(line) == str {
+ line = (type: line)
+ }
+
+ let line-type = line.at("type", default: "raw")
+ assert(line-type in ("raw", "linear", "spline", "vh", "hv", "hvh"))
+
+ // Transform data into line-data
+ let line-data = if line-type == "linear" {
+ return util.linearized-data(data, line.at("epsilon", default: 0))
+ } else if line-type == "spline" {
+ return util.sampled-spline-data(data,
+ line.at("tension", default: .5),
+ line.at("samples", default: 15))
+ } else if line-type == "vh" {
+ return hvh-data(0)
+ } else if line-type == "hv" {
+ return hvh-data(1)
+ } else if line-type == "hvh" {
+ return hvh-data(line.at("mid", default: .5))
+ } else {
+ return data
+ }
+}
+
+// Fill a plot by generating a fill path to y value `to`
+#let fill-segments-to(segments, to) = {
+ for s in segments {
+ let low = calc.min(..s.map(v => v.at(0)))
+ let high = calc.max(..s.map(v => v.at(0)))
+
+ let origin = (low, to)
+ let target = (high, to)
+
+ draw.line(origin, ..s, target, stroke: none)
+ }
+}
+
+// Fill a shape by generating a fill path for each segment
+#let fill-shape(paths) = {
+ for p in paths {
+ draw.line(..p, stroke: none)
+ }
+}
+
+// Prepare line data
+#let _prepare(self, ctx) = {
+ let (x, y) = (ctx.x, ctx.y)
+
+ // Generate stroke paths
+ self.stroke-paths = util.compute-stroke-paths(self.line-data, x, y)
+
+ // 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, y)
+ }
+
+ return self
+}
+
+// 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)
+ }
+}
+
+// Fill line data
+#let _fill(self, ctx) = {
+ let (x, y) = (ctx.x, ctx.y)
+
+ if self.hypograph {
+ fill-segments-to(self.fill-paths, y.min)
+ }
+ if self.epigraph {
+ fill-segments-to(self.fill-paths, y.max)
+ }
+ if self.fill {
+ if self.at("fill-type", default: "axis") == "shape" {
+ fill-shape(self.fill-paths)
+ } else {
+ fill-segments-to(self.fill-paths,
+ calc.max(calc.min(y.max, 0), y.min))
+ }
+ }
+}
+
+/// Add data to a plot environment.
+///
+/// Note: You can use this for scatter plots by setting
+/// the stroke style to `none`: `add(..., style: (stroke: none))`.
+///
+/// Must be called from the body of a `plot(..)` command.
+///
+/// - domain (domain): Domain of `data`, if `data` is a function. Has no effect
+/// if `data` is not a function.
+/// - hypograph (bool): Fill hypograph; uses the `hypograph` style key for
+/// drawing
+/// - epigraph (bool): Fill epigraph; uses the `epigraph` style key for
+/// drawing
+/// - fill (bool): Fill the shape of the plot
+/// - fill-type (string): Fill type:
+/// / `"axis"`: Fill the shape to y = 0
+/// / `"shape"`: Fill the complete shape
+/// - samples (int): Number of times the `data` function gets called for
+/// sampling y-values. Only used if `data` is of type function. This parameter gets
+/// passed onto `sample-fn`.
+/// - sample-at (array): Array of x-values the function gets sampled at in addition
+/// to the default sampling. This parameter gets passed to `sample-fn`.
+/// - line (string, dictionary): Line type to use. The following types are
+/// supported:
+/// / `"raw"`: Plot raw data
+/// / `"linear"`: Linearize data
+/// / `"spline"`: Calculate a Catmull-Rom curve through all points
+/// / `"vh"`: Move vertical and then horizontal
+/// / `"hv"`: Move horizontal and then vertical
+/// / `"hvh"`: Add a vertical step in the middle
+///
+/// If the value is a dictionary, the type must be
+/// supplied via the `type` key. The following extra
+/// attributes are supported:
+/// / `"samples" `: Samples of splines
+/// / `"tension" `: Tension of splines
+/// / `"mid" `: Mid-Point of hvh lines (0 to 1)
+/// / `"epsilon" `: Linearization slope epsilon for
+/// use with `"linear"`, defaults to 0.
+///
+/// ```cexample-vertical
+/// let points(offset: 0) = ((0,0), (1,1), (2,0), (3,1), (4,0)).map(((x,y)) => {
+/// (x,y + offset * 1.5)
+/// })
+/// plot.plot(size: (12, 3), axis-style: none, {
+/// plot.add(points(offset: 5), line: (type: "hvh", mid: .1))
+/// plot.add(points(offset: 4), line: "hvh")
+/// plot.add(points(offset: 3), line: "hv")
+/// plot.add(points(offset: 2), line: "vh")
+/// plot.add(points(offset: 1), line: "spline")
+/// plot.add(points(offset: 0), line: "linear")
+/// })
+/// ```
+///
+/// - style (style): Style to use, can be used with a `palette` function
+/// - axes (axes): Name of the axes to use for plotting. Reversing the axes
+/// means rotating the plot by 90 degrees.
+/// - mark (string): Mark symbol to place at each distinct value of the
+/// graph. Uses the `mark` style key of `style` for drawing:
+///
+/// ```cexample-vertical
+/// let points(offset) = ((offset, 0), (offset, 1))
+///
+/// plot.plot(size: (12, 2), axis-style: none, {
+/// plot.add(points(0), mark: "*") // same as "x"
+/// plot.add(points(1), mark: "square")
+/// plot.add(points(2), mark: "triangle")
+/// plot.add(points(3), mark: "o")
+/// plot.add(points(4), mark: "+")
+/// plot.add(points(5), mark: "-")
+/// plot.add(points(6), mark: "|")
+/// plot.add(points(7), mark: "<>")
+/// plot.add(points(8), mark: 5) // specify an integer to use a regular polygon.
+///
+/// // Alternatively, specify custom function:
+/// plot.add(points(9), mark: (pt, size, style) => {
+/// let top = (to: pt, rel: (0, size))
+/// let bot = (to: pt, rel: (0, -size))
+/// let left = (to: pt, rel: (-size/2, 0))
+/// let right = (to: pt, rel: (size/2, 0))
+/// cetz.draw.line(top, left, bot, right, close: true, ..style)
+/// })
+/// })
+/// ```
+///
+///
+/// - mark-size (float): Mark size in canvas units
+/// - data (array,function): Array of 2D data points (numeric) or a function
+/// of the form `x => y`, where `x` is a value in `domain`
+/// and `y` must be numeric or a 2D vector (for parametric functions).
+/// ```cexample
+/// plot.plot(size: (2, 2), axis-style: none, {
+/// // Using an array of points:
+/// plot.add(((0,0), (calc.pi/2,1),
+/// (1.5*calc.pi,-1), (2*calc.pi,0)))
+/// // Sampling a function:
+/// plot.add(domain: (0, 2*calc.pi), calc.sin)
+/// })
+/// ```
+/// - label (none,content): Legend label to show for this plot.
+#let add(domain: auto,
+ hypograph: false,
+ epigraph: false,
+ fill: false,
+ fill-type: "axis",
+ style: (:),
+ mark: none,
+ mark-size: .2,
+ mark-style: (:),
+ samples: 50,
+ sample-at: (),
+ line: "raw",
+ axes: ("x", "y"),
+ label: none,
+ data
+ ) = {
+ // If data is of type function, sample it
+ if type(data) == function {
+ data = sample.sample-fn(data, domain, samples, sample-at: sample-at)
+ }
+
+ // Transform data
+ let line-data = transform-lines(data, line)
+
+ // Get x-domain
+ let x-domain = (
+ calc.min(..line-data.map(t => t.at(0))),
+ calc.max(..line-data.map(t => t.at(0)))
+ )
+
+ // Get y-domain
+ let y-domain = if line-data != none {(
+ calc.min(..line-data.map(t => t.at(1))),
+ calc.max(..line-data.map(t => t.at(1)))
+ )}
+
+ ((
+ type: "line",
+ label: label,
+ data: data, /* Raw data */
+ line-data: line-data, /* Transformed data */
+ axes: axes,
+ x-domain: x-domain,
+ y-domain: y-domain,
+ epigraph: epigraph,
+ hypograph: hypograph,
+ fill: fill,
+ fill-type: fill-type,
+ style: style,
+ mark: mark,
+ mark-size: mark-size,
+ mark-style: mark-style,
+ plot-prepare: _prepare,
+ plot-stroke: _stroke,
+ plot-fill: _fill,
+ plot-legend-preview: self => {
+ if self.fill or self.epigraph or self.hypograph {
+ draw.rect((0,0), (1,1), ..self.style)
+ } else {
+ draw.line((0,.5), (1,.5), ..self.style)
+ }
+ }
+ ),)
+}
+
+/// Add horizontal lines at one or more y-values. Every lines start and end points
+/// are at their axis bounds.
+///
+/// ```cexample
+/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, {
+/// plot.add(domain: (0, 4*calc.pi), calc.sin)
+/// // Add 3 horizontal lines
+/// plot.add-hline(-.5, 0, .5)
+/// })
+/// ```
+///
+/// - ..y (float): Y axis value(s) to add a line at
+/// - min (auto,float): X axis minimum value or auto to take the axis minimum
+/// - max (auto,float): X axis maximum value or auto to take the axis maximum
+/// - 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,
+ min: auto,
+ max: auto,
+ axes: ("x", "y"),
+ style: (:),
+ label: none,
+ ) = {
+ assert(y.pos().len() >= 1,
+ message: "Specify at least one y value")
+ assert(y.named().len() == 0)
+
+ let prepare(self, ctx) = {
+ let (x-min, x-max) = (ctx.x.min, ctx.x.max)
+ let (y-min, y-max) = (ctx.y.min, ctx.y.max)
+ let x-min = if min == auto { x-min } else { min }
+ let x-max = if max == auto { x-max } else { max }
+
+ self.lines = self.y.filter(y => y >= y-min and y <= y-max)
+ .map(y => ((x-min, y), (x-max, y)))
+ return self
+ }
+
+ let stroke(self, ctx) = {
+ for (a, b) in self.lines {
+ draw.line(a, b, fill: none)
+ }
+ }
+
+ let x-min = if min == auto { none } else { min }
+ let x-max = if max == auto { none } else { max }
+
+ ((
+ type: "hline",
+ label: label,
+ y: y.pos(),
+ x-domain: (x-min, x-max),
+ y-domain: (calc.min(..y.pos()), calc.max(..y.pos())),
+ axes: axes,
+ style: style,
+ plot-prepare: prepare,
+ plot-stroke: stroke,
+ ),)
+}
+
+/// Add vertical lines at one or more x-values. Every lines start and end points
+/// are at their axis bounds.
+///
+/// ```cexample
+/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, {
+/// plot.add(domain: (0, 2*calc.pi), calc.sin)
+/// // Add 3 vertical lines
+/// plot.add-vline(calc.pi/2, calc.pi, 3*calc.pi/2)
+/// })
+/// ```
+///
+/// - ..x (float): X axis values to add a line at
+/// - min (auto,float): Y axis minimum value or auto to take the axis minimum
+/// - max (auto,float): Y axis maximum value or auto to take the axis maximum
+/// - axes (array): Name of the axes to use for plotting, 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
+/// - label (none,content): Legend label to show for this plot.
+#let add-vline(..x,
+ min: auto,
+ max: auto,
+ axes: ("x", "y"),
+ style: (:),
+ label: none,
+ ) = {
+ assert(x.pos().len() >= 1,
+ message: "Specify at least one x value")
+ assert(x.named().len() == 0)
+
+ let prepare(self, ctx) = {
+ let (x-min, x-max) = (ctx.x.min, ctx.x.max)
+ let (y-min, y-max) = (ctx.y.min, ctx.y.max)
+ let y-min = if min == auto { y-min } else { min }
+ let y-max = if max == auto { y-max } else { max }
+
+ self.lines = self.x.filter(x => x >= x-min and x <= x-max)
+ .map(x => ((x, y-min), (x, y-max)))
+ return self
+ }
+
+ let stroke(self, ctx) = {
+ for (a, b) in self.lines {
+ draw.line(a, b, fill: none)
+ }
+ }
+
+ let y-min = if min == auto { none } else { min }
+ let y-max = if max == auto { none } else { max }
+
+ ((
+ type: "vline",
+ label: label,
+ x: x.pos(),
+ x-domain: (calc.min(..x.pos()), calc.max(..x.pos())),
+ y-domain: (y-min, y-max),
+ axes: axes,
+ style: style,
+ plot-prepare: prepare,
+ plot-stroke: stroke
+ ),)
+}
+
+/// Fill the area between two graphs. This behaves same as `add` but takes
+/// a pair of data instead of a single data array/function.
+/// The area between both function plots gets filled. For a more detailed
+/// explanation of the arguments, see @@add().
+///
+/// This can be used to display an error-band of a function.
+///
+/// ```cexample
+/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, {
+/// plot.add-fill-between(domain: (0, 2*calc.pi),
+/// calc.sin, // First function/data
+/// calc.cos) // Second function/data
+/// })
+/// ```
+///
+/// - domain (domain): Domain of both `data-a` and `data-b`. The domain is used for
+/// sampling functions only and has no effect on data arrays.
+/// - samples (int): Number of times the `data-a` and `data-b` function gets called for
+/// sampling y-values. Only used if `data-a` or `data-b` is of
+/// type function.
+/// - sample-at (array): Array of x-values the function(s) get sampled at in addition
+/// to the default sampling.
+/// - line (string, dictionary): Line type to use, see @@add().
+/// - style (style): Style to use, can be used with a palette function.
+/// - label (none,content): Legend label to show for this plot.
+/// - 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,
+ data-b,
+ domain: auto,
+ samples: 50,
+ sample-at: (),
+ line: "raw",
+ axes: ("x", "y"),
+ label: none,
+ style: (:)) = {
+ // If data is of type function, sample it
+ if type(data-a) == function {
+ data-a = sample.sample-fn(data-a, domain, samples, sample-at: sample-at)
+ }
+ if type(data-b) == function {
+ data-b = sample.sample-fn(data-b, domain, samples, sample-at: sample-at)
+ }
+
+ // Transform data
+ let line-a-data = transform-lines(data-a, line)
+ let line-b-data = transform-lines(data-b, line)
+
+ // Get x-domain
+ let x-domain = (
+ calc.min(..line-a-data.map(t => t.at(0)),
+ ..line-b-data.map(t => t.at(0))),
+ calc.max(..line-a-data.map(t => t.at(0)),
+ ..line-b-data.map(t => t.at(0)))
+ )
+
+ // Get y-domain
+ let y-domain = if line-a-data != none and line-b-data != none {(
+ calc.min(..line-a-data.map(t => t.at(1)),
+ ..line-b-data.map(t => t.at(1))),
+ calc.max(..line-a-data.map(t => t.at(1)),
+ ..line-b-data.map(t => t.at(1)))
+ )}
+
+ let prepare(self, ctx) = {
+ let (x, y) = (ctx.x, ctx.y)
+
+ // Generate stroke paths
+ self.stroke-paths = (
+ a: util.compute-stroke-paths(self.line-data.a, x, y),
+ b: util.compute-stroke-paths(self.line-data.b, x, y),
+ )
+
+ // Generate fill paths
+ self.fill-paths = util.compute-fill-paths(self.line-data.a + self.line-data.b.rev(), x, y)
+
+ return self
+ }
+
+ let stroke(self, ctx) = {
+ for p in self.stroke-paths.a {
+ draw.line(..p, fill: none)
+ }
+ for p in self.stroke-paths.b {
+ draw.line(..p, fill: none)
+ }
+ }
+
+ let fill(self, ctx) = {
+ fill-shape(self.fill-paths)
+ }
+
+ ((
+ type: "fill-between",
+ label: label,
+ axes: axes,
+ line-data: (a: line-a-data, b: line-b-data),
+ x-domain: x-domain,
+ y-domain: y-domain,
+ style: 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/packages/preview/cetz-plot/0.1.4/src/plot/mark.typ b/packages/preview/cetz-plot/0.1.4/src/plot/mark.typ
new file mode 100644
index 0000000000..c401138ba7
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/mark.typ
@@ -0,0 +1,52 @@
+#import "/src/cetz.typ": draw
+#import "/src/axes.typ"
+
+// Draw mark at point with size
+#let draw-mark-shape(pt, size, mark, style) = {
+ let sx = size
+ let sy = size
+
+ let bl(pt) = (rel: (-sx/2, -sy/2), to: pt)
+ let br(pt) = (rel: (sx/2, -sy/2), to: pt)
+ let tl(pt) = (rel: (-sx/2, sy/2), to: pt)
+ let tr(pt) = (rel: (sx/2, sy/2), to: pt)
+ let ll(pt) = (rel: (-sx/2, 0), to: pt)
+ let rr(pt) = (rel: (sx/2, 0), to: pt)
+ let tt(pt) = (rel: (0, sy/2), to: pt)
+ let bb(pt) = (rel: (0, -sy/2), to: pt)
+
+ if mark == "o" {
+ draw.circle(pt, radius: (sx/2, sy/2), ..style)
+ } else if mark == "square" {
+ draw.rect(bl(pt), tr(pt), ..style)
+ } else if mark == "triangle" {
+ draw.line(bl(pt), br(pt), tt(pt), close: true, ..style)
+ } else if mark == "*" or mark == "x" {
+ draw.line(bl(pt), tr(pt), ..style)
+ draw.line(tl(pt), br(pt), ..style)
+ } else if mark == "+" {
+ draw.line(ll(pt), rr(pt), ..style);
+ draw.line(tt(pt), bb(pt), ..style)
+ } else if mark == "-" {
+ draw.line(ll(pt), rr(pt), ..style)
+ } else if mark == "|" {
+ draw.line(tt(pt), bb(pt), ..style)
+ } else if mark == "diamond" or mark == "<>" {
+ draw.line(ll(pt), tt(pt), rr(pt), bb(pt), close: true, ..style)
+ } else if type(mark) == int and mark > 2 {
+ let pts = range(mark).map(i => (to: pt, rel: (90deg + 360deg * i / mark, size/2)))
+ draw.line(..pts, close: true, ..style)
+ } else if type(mark) == function {
+ mark(pt, size, style)
+ }
+}
+
+#let draw-mark(pts, x, y, mark, mark-size, plot-size) = {
+ let pts = pts.map(pt => {
+ axes.transform-vec(plot-size, x, y, none, pt)
+ }).filter(pt => pt != none)
+
+ for pt in pts {
+ draw-mark-shape(pt, mark-size, mark, (:))
+ }
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/sample.typ b/packages/preview/cetz-plot/0.1.4/src/plot/sample.typ
new file mode 100644
index 0000000000..3ad881d73b
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/sample.typ
@@ -0,0 +1,79 @@
+/// Sample the given single parameter function `samples` times, with values
+/// evenly spaced within the range given by `domain` and return each
+/// sampled `y` value in an array as `(x, y)` tuple.
+///
+/// If the functions first return value is a tuple `(x, y)`, then all return values
+/// must be a tuple.
+///
+/// - fn (function): Function to sample of the form `(x) => y` or `(t) => (x, y)`, where
+/// `x` or `t` are `float` values within the domain specified by `domain`.
+/// - domain (domain): Domain of `fn` used as bounding interval for the sampling points.
+/// - samples (int): Number of samples in domain.
+/// - sample-at (array): List of x values the function gets sampled at in addition
+/// to the `samples` number of samples. Values outsides the
+/// specified domain are legal.
+/// -> array: Array of (x, y) tuples
+#let sample-fn(fn, domain, samples, sample-at: ()) = {
+ assert(samples + sample-at.len() >= 2,
+ message: "You must at least sample 2 values")
+ assert(type(domain) == array and domain.len() == 2,
+ message: "Domain must be a tuple")
+
+ let (lo, hi) = domain
+
+ let y0 = (fn)(lo)
+ let is-vector = type(y0) == array
+ if not is-vector {
+ y0 = ((lo, y0), )
+ } else {
+ y0 = (y0, )
+ }
+
+ let pts = sample-at + range(0, samples).map(t => lo + t / (samples - 1) * (hi - lo))
+ pts = pts.sorted()
+
+ return pts.map(x => {
+ if is-vector {
+ (fn)(x)
+ } else {
+ (x, (fn)(x))
+ }
+ })
+}
+
+/// Samples the given two parameter function with `x-samples` and
+/// `y-samples` values evenly spaced within the range given by
+/// `x-domain` and `y-domain` and returns each sampled output in
+/// an array.
+///
+/// - fn (function): Function of the form `(x, y) => z` with all values being numbers.
+/// - x-domain (domain): Domain used as bounding interval for sampling point's x
+/// values.
+/// - y-domain (domain): Domain used as bounding interval for sampling point's y
+/// values.
+/// - x-samples (int): Number of samples in the x-domain.
+/// - y-samples (int): Number of samples in the y-domain.
+/// -> array: Array of z scalars
+#let sample-fn2(fn, x-domain, y-domain, x-samples, y-samples) = {
+ assert(x-samples >= 2,
+ message: "You must at least sample 2 x-values")
+ assert(y-samples >= 2,
+ message: "You must at least sample 2 y-values")
+ assert(type(x-domain) == array and x-domain.len() == 2,
+ message: "X-Domain must be a tuple")
+ assert(type(y-domain) == array and y-domain.len() == 2,
+ message: "Y-Domain must be a tuple")
+
+ let (x-min, x-max) = x-domain
+ let (y-min, y-max) = y-domain
+ let y-pts = range(0, y-samples)
+ let x-pts = range(0, x-samples)
+
+ return y-pts.map(y => {
+ let y = y / (y-samples - 1) * (y-max - y-min) + y-min
+ return x-pts.map(x => {
+ let x = x / (x-samples - 1) * (x-max - x-min) + x-min
+ return float((fn)(x, y))
+ })
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/util.typ b/packages/preview/cetz-plot/0.1.4/src/plot/util.typ
new file mode 100644
index 0000000000..8d0b03ef80
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/util.typ
@@ -0,0 +1,372 @@
+#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
+/// - x (axis): X-Axis
+/// - y (axis): Y-Axis
+/// -> array List of stroke paths
+#let compute-stroke-paths(points, x, y) = {
+ clipped-paths(points, (x.min, y.min), (x.max, y.max), fill: false)
+}
+
+/// Compute clipped fill path
+///
+/// - points (array): X/Y data points
+/// - x (axis): X-Axis
+/// - y (axis): Y-Axis
+/// -> array List of fill paths
+#let compute-fill-paths(points, x, y) = {
+ clipped-paths(points, (x.min, y.min), (x.max, y.max), fill: true)
+}
+
+/// Return points of a sampled catmull-rom through the
+/// input points.
+///
+/// - points (array): Array of input vectors
+/// - tension (float): Catmull-Rom tension
+/// - samples (int): Number of samples
+/// -> array Array of vectors
+#let sampled-spline-data(points, tension, samples) = {
+ assert(samples >= 1 and samples <= 100,
+ message: "Must at least use 1 sample per curve")
+
+ let curves = bezier.catmull-to-cubic(points, tension)
+ let pts = ()
+ for c in curves {
+ for t in range(0, samples + 1) {
+ let t = t / samples
+ pts.push(bezier.cubic-point(..c, t))
+ }
+ }
+ return pts
+}
+
+/// Simplify linear data by "detecting" linear sections
+/// and skipping points until the slope changes.
+/// This can have a huge impact on the number of lines
+/// getting rendered.
+///
+/// - data (array): Data points
+/// - epsilon (float): Curvature threshold to treat data as linear
+#let linearized-data(data, epsilon) = {
+ let pts = ()
+ // Current slope, set to none if infinite
+ let dx = none
+ // Previous point, last skipped point
+ let prev = none
+ let skipped = none
+ // Current direction
+ let dir = 0
+
+ let len = data.len()
+ for i in range(0, len) {
+ let pt = data.at(i)
+ if prev != none and i < len - 1 {
+ let new-dir = pt.at(0) - prev.at(0)
+ if new-dir == 0 {
+ // Infinite slope
+ if dx != none {
+ if skipped != none {pts.push(skipped); skipped = none}
+ pts.push(pt)
+ } else {
+ skipped = pt
+ }
+ dx = none
+ } else {
+ // Push the previous and the current point
+ // if slope or direction changed
+ let new-dx = ((pt.at(1) - prev.at(1)) / new-dir)
+ if dx == none or calc.abs(new-dx - dx) > epsilon or (new-dir * dir) < 0 {
+ if skipped != none {pts.push(skipped); skipped = none}
+ pts.push(pt)
+
+ dx = new-dx
+ dir = new-dir
+ } else {
+ skipped = pt
+ }
+ }
+ } else {
+ if skipped != none {pts.push(skipped); skipped = none}
+ pts.push(pt)
+ }
+
+ prev = pt
+ }
+
+ return pts
+}
+
+// Get the default axis orientation
+// depending on the axis name
+#let get-default-axis-horizontal(name) = {
+ return lower(name).starts-with("x")
+}
+
+// Setup axes dictionary
+//
+// - axis-dict (dictionary): Existing axis dictionary
+// - options (dictionary): Named arguments
+// - plot-size (tuple): Plot width, height tuple
+#let setup-axes(ctx, axis-dict, options, plot-size) = {
+ import "/src/axes.typ"
+
+ // 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 }
+ }
+
+ for (name, axis) in axis-dict {
+ if not "ticks" in axis { axis.ticks = () }
+ axis.label = get-axis-option(name, "label", $italic(name)$)
+
+ // Configure axis bounds
+ axis.min = get-axis-option(name, "min", axis.min)
+ axis.max = get-axis-option(name, "max", axis.max)
+
+ 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
+ }
+
+ axis.mode = get-axis-option(name, "mode", "lin")
+ axis.base = get-axis-option(name, "base", 10)
+
+ // Configure axis orientation
+ axis.horizontal = get-axis-option(name, "horizontal",
+ get-default-axis-horizontal(name))
+
+ // Configure ticks
+ axis.ticks.list = get-axis-option(name, "ticks", ())
+ axis.ticks.step = get-axis-option(name, "tick-step", axis.ticks.step)
+ axis.ticks.minor-step = get-axis-option(name, "minor-tick-step", axis.ticks.minor-step)
+ axis.ticks.decimals = get-axis-option(name, "decimals", 2)
+ axis.ticks.unit = get-axis-option(name, "unit", [])
+ axis.ticks.format = get-axis-option(name, "format", axis.ticks.format)
+
+ // Axis break
+ axis.show-break = get-axis-option(name, "break", false)
+ axis.inset = get-axis-option(name, "inset", (0, 0))
+
+ // Configure grid
+ axis.ticks.grid = get-axis-option(name, "grid", false)
+
+ axis-dict.at(name) = axis
+ }
+
+ // Set axis options round two, after setting
+ // axis bounds
+ for (name, axis) in axis-dict {
+ let changed = false
+
+ // Configure axis aspect ratio
+ let equal-to = get-axis-option(name, "equal", none)
+ if equal-to != none {
+ assert.eq(type(equal-to), str,
+ message: "Expected axis name.")
+ assert(equal-to != name,
+ message: "Axis can not be equal to itself.")
+
+ let other = axis-dict.at(equal-to, default: none)
+ assert(other != none,
+ message: "Other axis must exist.")
+ assert(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
+ }
+
+ if changed {
+ axis-dict.at(name) = axis
+ }
+ }
+
+ for (name, axis) in axis-dict {
+ axis-dict.at(name) = axes.prepare-axis(ctx, axis, name)
+ }
+
+ return axis-dict
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/plot/violin.typ b/packages/preview/cetz-plot/0.1.4/src/plot/violin.typ
new file mode 100644
index 0000000000..5da01e9554
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/plot/violin.typ
@@ -0,0 +1,134 @@
+#import "/src/cetz.typ": draw
+#import "util.typ"
+#import "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)))
+}
+
+#let _violin-render(self, ctx, violin, filling: true) = {
+ let path = range(self.samples)
+ .map((t)=>violin.min + (violin.max - violin.min) * (t / self.samples ))
+ .map((u)=>(u, (violin.convolve)(u)))
+ .map(((u,v)) => {
+ (violin.x-position + v, u)
+ })
+
+ if self.side == "both"{
+ path += path.rev().map(((x,y))=> {(2 * violin.x-position - x,y)})
+ } else if self.side == "left"{
+ path = path.map(((x,y)) => (2 * violin.x-position - x,y))
+ }
+
+ let stroke-paths = util.compute-stroke-paths(path, ctx.x, ctx.y)
+
+ for p in stroke-paths{
+ let args = arguments(..p, closed: self.side == "both")
+ if filling {
+ args = arguments(..args, stroke: none)
+ } else {
+ args = arguments(..args, fill: none)
+ }
+ draw.line(..self.style, ..args)
+ }
+}
+
+#let _plot-prepare(self, ctx) = {
+ self.violins = self.data.map(entry=> {
+ let points = entry.at(self.y-key)
+ let (min, max) = (calc.min(..points), calc.max(..points))
+ let range = calc.abs(max - min)
+ (
+ x-position: entry.at(self.x-key),
+ points: points,
+ length: points.len(),
+ min: min - (self.extents * range),
+ max: max + (self.extents * range),
+ convolve: (t) => {
+ points.map(y => (self.kernel)((y - t) / self.bandwidth)).sum() / (points.len() * self.bandwidth)
+ }
+ )
+ })
+ return self
+}
+
+#let _plot-stroke(self, ctx) = {
+ for violin in self.violins {
+ _violin-render(self, ctx, violin, filling: false)
+ }
+}
+
+#let _plot-fill(self, ctx) = {
+ for violin in self.violins {
+ _violin-render(self, ctx, violin, filling: true)
+ }
+}
+
+#let _plot-legend-preview(self) = {
+ draw.rect((0,0), (1,1), ..self.style)
+}
+
+
+/// Add a violin plot
+///
+/// A violin plot is a chart that can be used to compare the distribution of continuous
+/// data between categories.
+///
+/// - data (array): Array of data items. An item is an array containing an `x` and one
+/// or more `y` values.
+/// - x-key (int, string): Key to use for retrieving the `x` position of the violin.
+/// - y-key (int, string): Key to use for retrieving values of points within the category.
+/// - side (string): The sides of the violin to be rendered:
+/// / left: Plot only the left side of the violin.
+/// / right: Plot only the right side of the violin.
+/// / both: Plot both sides of the violin.
+/// - kernel (function): The kernel density estimator function, which takes a single
+/// `x` value relative to the center of a distribution (0) and
+/// normalized by the bandwidth
+/// - bandwidth (float): The smoothing parameter of the kernel.
+/// - extents (float): The extension of the domain, expressed as a fraction of spread.
+/// - samples (int): The number of samples of the kernel to render.
+/// - style (dictionary): Style override dictionary.
+/// - 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(
+ data,
+ x-key: 0,
+ y-key: 1,
+ side: "right",
+ kernel: kernel-normal.with(stdev: 1.5),
+ bandwidth: 1,
+ extents: 0.25,
+
+ samples: 50,
+ style: (:),
+ mark-style: (:),
+ axes: ("x", "y"),
+ label: none,
+) = {
+
+ ((
+ type: "violins",
+
+ data: data,
+ x-key: x-key,
+ y-key: y-key,
+ side: side,
+ kernel: kernel,
+ bandwidth: bandwidth,
+ extents: extents,
+
+ samples: samples,
+ style: style,
+ mark-style: mark-style,
+ axes: axes,
+ label: label,
+
+ plot-prepare: _plot-prepare,
+ plot-stroke: _plot-stroke,
+ plot-fill: _plot-fill,
+ plot-legend-preview: _plot-legend-preview,
+ ),)
+
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/smartart.typ b/packages/preview/cetz-plot/0.1.4/src/smartart.typ
new file mode 100644
index 0000000000..32c7e21b38
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/smartart.typ
@@ -0,0 +1,2 @@
+#import "smartart/process.typ"
+#import "smartart/cycle.typ"
\ No newline at end of file
diff --git a/packages/preview/cetz-plot/0.1.4/src/smartart/common.typ b/packages/preview/cetz-plot/0.1.4/src/smartart/common.typ
new file mode 100644
index 0000000000..9b6f8192fb
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/smartart/common.typ
@@ -0,0 +1,440 @@
+#import "/src/cetz.typ" as cetz: draw, coordinate, util.resolve-number, vector
+
+/// Possible chevron caps: #cetz.smartart.process.CHEVRON-CAPS
+/// ```cexample
+/// for cap in smartart.process.CHEVRON-CAPS {
+/// smartart.process.chevron(
+/// ([Step 1], [Step 2]), spacing: 0,
+/// start-cap: cap, middle-cap: cap, end-cap: cap)
+/// translate(y: -1)
+/// }
+/// ```
+#let CHEVRON-CAPS = (
+ "(", "<", "|", ">", ")"
+)
+
+#let _draw-arrow(
+ start,
+ end,
+ height,
+ fill,
+ stroke,
+ double: false,
+ name: none
+) = {
+ let h2 = height / 2
+ let h4 = height / 4
+ draw.group(name: name, ctx => {
+ let (ctx, p1) = coordinate.resolve(ctx, start)
+ let (ctx, p2) = coordinate.resolve(ctx, end)
+ let (x1, y1, _) = p1
+ let (x2, y2, _) = p2
+ let v = vector.sub(p2, p1)
+ let d = vector.norm(v)
+ let n = (-d.at(1), d.at(0))
+ let len = vector.len(v)
+ let head-len = h2
+ /*
+ c
+ a---b\
+ p1 p2
+ g---f/
+ e
+ */
+ /*
+ i c
+ /a---b\
+ p1 p2
+ \g---f/
+ h e
+ */
+ let a = vector.add(p1, vector.scale(n, h4))
+ if double {
+ a = vector.add(
+ a,
+ vector.scale(d, head-len)
+ )
+ }
+ let b = vector.add(
+ a,
+ vector.scale(
+ d,
+ len - if double {2 * head-len} else {head-len}
+ )
+ )
+ let c = vector.add(b, vector.scale(n, h4))
+ let e = vector.add(c, vector.scale(n, -height))
+ let f = vector.add(e, vector.scale(n, h4))
+ let g = vector.sub(a, vector.scale(n, h2))
+ let pts = (
+ a, b, c, p2, e, f, g
+ )
+ if double {
+ let h = vector.sub(g, vector.scale(n, h4))
+ let i = vector.add(a, vector.scale(n, h4))
+ pts += (h, p1, i)
+ }
+
+ draw.line(
+ ..pts,
+ stroke: stroke,
+ fill: fill,
+ close: true
+ )
+ draw.anchor("start", p1)
+ draw.anchor("end", p2)
+ draw.anchor("center", (p1, 50%, p2))
+ draw.anchor("default", (p1, 50%, p2))
+ })
+}
+
+#let _draw-chevron(
+ start,
+ end,
+ thickness,
+ fill,
+ stroke,
+ start-cap,
+ end-cap,
+ cap-ratio,
+ offset-start,
+ offset-end,
+ name: none
+) = {
+ let h2 = thickness / 2
+ let h4 = thickness / 4
+ draw.group(name: name, ctx => {
+ let (ctx, p1) = coordinate.resolve(ctx, start)
+ let (ctx, p2) = coordinate.resolve(ctx, end)
+
+
+ draw.anchor("center", (p1, 50%, p2))
+ draw.anchor("default", (p1, 50%, p2))
+
+ let v = vector.sub(p2, p1)
+ let d = vector.norm(v)
+ let n = (d.at(1), d.at(0))
+
+ /*
+ > | < )
+ sa sa sa sa,
+ \ | / \
+ sb | sb |sb
+ / | \ /
+ sz sz sz sz´
+ */
+
+ let cap-width = thickness * cap-ratio / 100%
+
+ if offset-start and start-cap in ("(", "<") {
+ p1 = vector.add(p1, vector.scale(d, cap-width))
+ }
+
+ if offset-end and end-cap in (")", ">") {
+ p2 = vector.sub(p2, vector.scale(d, cap-width))
+ }
+
+ // Start cap
+ let sb = if start-cap in ("(", "<") {
+ vector.sub(p1, vector.scale(d, cap-width))
+ } else {
+ p1
+ }
+ let sa = vector.add(p1, vector.scale(n, h2))
+ if start-cap in (")", ">") {
+ sa = vector.sub(sa, vector.scale(d, cap-width))
+ }
+ let sz = vector.sub(sa, vector.scale(n, thickness))
+
+ // End cap
+ let eb = if end-cap in (")", ">") {
+ vector.add(p2, vector.scale(d, cap-width))
+ } else {
+ p2
+ }
+ let ea = vector.add(p2, vector.scale(n, h2))
+ if end-cap in ("(", "<") {
+ ea = vector.add(ea, vector.scale(d, cap-width))
+ }
+ let ez = vector.sub(ea, vector.scale(n, thickness))
+
+ draw.merge-path(
+ {
+ // Start cap
+ if start-cap in ("(", ")") {
+ draw.arc-through(sa, sb, sz)
+ } else if start-cap == "|" {
+ draw.line(sa, sz)
+ } else {
+ draw.line(sa, sb, sz)
+ }
+
+ // End cap
+ if end-cap in ("(", ")") {
+ draw.arc-through(ez, eb, ea)
+ } else if end-cap == "|" {
+ draw.line(ez, ea)
+ } else {
+ draw.line(ez, eb, ea)
+ }
+ },
+ stroke: stroke,
+ fill: fill,
+ close: true
+ )
+ draw.anchor("start", p1)
+ draw.anchor("end", p2)
+ })
+}
+
+#let _get-steps-sizes(steps, ctx, style, step-style-at) = {
+ let sizes = steps.enumerate().map(p => {
+ let (i, step) = p
+ let step-style = style.steps + step-style-at(i)
+ let padding = resolve-number(ctx, step-style.padding)
+ let max-width = resolve-number(ctx, step-style.max-width)
+ max-width -= 2 * padding
+ let m = measure(step, width: max-width * ctx.length)
+ let w = resolve-number(ctx, m.width)
+ let h = resolve-number(ctx, m.height)
+ return (w, h)
+ })
+
+ let largest-width = calc.max(..sizes.map(s => s.first()))
+ let highest-height = calc.max(..sizes.map(s => s.last()))
+
+ return (sizes, largest-width, highest-height)
+}
+
+#let _get-style-at-func(style, n-steps) = {
+ if type(style) == function {
+ style
+ } else if type(style) == array {
+ i => {
+ let s = style.at(calc.rem(i, style.len()))
+ if type(s) == color or type(s) == gradient {
+ (fill: s)
+ } else {
+ s
+ }
+ }
+ } else if type(style) == gradient {
+ i => (fill: style.sample(i / (n-steps - 1) * 100%))
+ } else {
+ i => (:)
+ }
+}
+
+#let _pos-to-anchor(pos) = {
+ if pos == left {return "west"}
+ if pos == right {return "east"}
+ if pos == top {return "north"}
+ if pos == bottom {return "south"}
+ panic("Cannot convert alignment " + repr(pos) + " to cardinal anchor")
+}
+
+#let _dir-to-anchors(dir) = {
+ return (
+ _pos-to-anchor(dir.start()),
+ _pos-to-anchor(dir.end())
+ )
+}
+
+#let _dir-to-str(dir) = {
+ if dir == ttb {return "ttb"}
+ if dir == btt {return "btt"}
+ if dir == ltr {return "ltr"}
+ if dir == rtl {return "rtl"}
+ panic("Invalid direction " + repr(dir))
+}
+
+#let _draw-step-content(step, name, width) = {
+ draw.content(
+ name + ".center",
+ box(
+ width: width,
+ align(center)[
+ #set text(bottom-edge: "baseline")
+ #step
+ ]
+ ),
+ anchor: "center"
+ )
+}
+
+#let _draw-step-frame(ctx, center, style, name, w, h) = {
+ let padding = resolve-number(ctx, style.padding)
+ let radius = resolve-number(ctx, style.radius)
+
+ if style.shape == "rect" {
+ let tl = (
+ rel: (-w / 2 - padding, h / 2 + padding),
+ to: center
+ )
+ let br = (
+ rel: (w / 2 + padding, -h / 2 - padding),
+ to: center
+ )
+
+ draw.rect(
+ tl, br,
+ name: name,
+ stroke: style.stroke,
+ fill: style.fill,
+ radius: radius
+ )
+ } else if style.shape == "circle" {
+ let w2 = w + padding * 2
+ let h2 = h + padding * 2
+ draw.circle(
+ center,
+ name: name,
+ radius: calc.sqrt(w2 * w2 + h2 * h2) / 2,
+ stroke: style.stroke,
+ fill: style.fill
+ )
+ } else if style.shape == none {
+ let tl = (
+ rel: (-w / 2 - padding, h / 2 + padding),
+ to: center
+ )
+ let br = (
+ rel: (w / 2 + padding, -h / 2 - padding),
+ to: center
+ )
+ draw.hide(draw.rect(
+ tl, br,
+ name: name,
+ stroke: none,
+ fill: none
+ ))
+ }
+}
+
+#let _draw-step(ctx, step, pos, style, name, w, h, dir: none) = {
+ /*
+ let padding = resolve-number(ctx, style.padding)
+ let radius = resolve-number(ctx, style.radius)
+
+ let tl = (
+ rel: (
+ ltr: (0, h / 2 + padding),
+ rtl: (-w - padding * 2, h / 2 + padding),
+ btt: (-w / 2 - padding, h + 2 * padding),
+ ttb: (-w / 2 - padding, 0),
+ ).at(_dir-to-str(dir)),
+ to: pos
+ )
+ let br = (
+ rel: (w + padding * 2, -h - padding * 2),
+ to: tl
+ )
+
+ draw.rect(
+ tl, br,
+ name: name,
+ stroke: style.stroke,
+ fill: style.fill,
+ radius: radius
+ )*/
+ if dir != none {
+ let padding = resolve-number(ctx, style.padding)
+ pos = (
+ rel: (
+ ltr: (w / 2 + padding, 0),
+ rtl: (-w / 2 - padding, 0),
+ btt: (0, h / 2 + padding),
+ ttb: (0, -h / 2 - padding),
+ ).at(_dir-to-str(dir)),
+ to: pos
+ )
+ }
+ _draw-step-frame(ctx, pos, style, name, w, h)
+ _draw-step-content(step, name, w * ctx.length)
+}
+
+#let _draw-arc-arrow(
+ start-angle,
+ end-angle,
+ radius,
+ height,
+ fill,
+ stroke,
+ double: false,
+ name: none
+) = {
+ let h2 = height / 2
+ let h4 = height / 4
+ let angle-range = end-angle - start-angle
+ // Angle for a length of h/2
+ let arrow-angle = (h2 / radius) * (180deg / calc.pi)
+ if angle-range < 0deg {
+ arrow-angle *= -1
+ }
+ draw.group(name: name, ctx => {
+ let pre-end-angle = end-angle - arrow-angle
+ let mid-angle = start-angle + (angle-range - arrow-angle) / 2
+ let radius-int = radius - h4
+ let radius-ext = radius + h4
+ let radius-int2 = radius - h2
+ let radius-ext2 = radius + h2
+ let post-start-angle = start-angle + arrow-angle
+
+ /*
+ c re2
+ a-m1-b\ re
+ p1 p2 r
+ g-m2-f/ ri
+ e ri2
+ */
+
+ /*
+ i c re2
+ /a-m1-b\ re
+ p1 p2 r
+ \g-m2-f/ ri
+ h e ri2
+ */
+ let p1 = (start-angle, radius)
+ let a = (
+ if double {post-start-angle} else {start-angle},
+ radius-ext
+ )
+ let m1 = (mid-angle, radius-ext)
+ let b = (pre-end-angle, radius-ext)
+ let p2 = (end-angle, radius)
+ let f = (pre-end-angle, radius-int)
+ let m2 = (mid-angle, radius-int)
+ let g = (
+ if double {post-start-angle} else {start-angle},
+ radius-int
+ )
+
+ let c = (pre-end-angle, radius-ext2)
+ let e = (pre-end-angle, radius-int2)
+
+ draw.merge-path(
+ {
+ draw.arc-through(a, m1, b)
+ draw.line((), c, p2, e, f)
+ draw.arc-through((), m2, g)
+ if double {
+ let h = (post-start-angle, radius-int2)
+ let i = (post-start-angle, radius-ext2)
+ draw.line((), h, p1, i)
+ }
+ },
+ stroke: stroke,
+ fill: fill,
+ close: true
+ )
+
+ let center = (mid-angle, radius)
+ draw.anchor("start", p1)
+ draw.anchor("end", p2)
+ draw.anchor("center", center)
+ draw.anchor("center-ext", (mid-angle, radius-ext))
+ draw.anchor("center-int", (mid-angle, radius-int))
+ draw.anchor("default", center)
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/smartart/cycle.typ b/packages/preview/cetz-plot/0.1.4/src/smartart/cycle.typ
new file mode 100644
index 0000000000..0ebbff03dd
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/smartart/cycle.typ
@@ -0,0 +1,324 @@
+#import "/src/cetz.typ" as cetz: draw, styles, palette, coordinate, util.resolve-number, vector
+
+#import "common.typ": *
+
+#let cycle-basic-default-style = (
+ stroke: auto,
+ fill: auto,
+ steps: (
+ stroke: none,
+ fill: none,
+ radius: 0.2em,
+ padding: 0.6em,
+ max-width: 5em,
+ shape: "rect"
+ ),
+ arrows: (
+ stroke: none,
+ fill: "steps",
+ thickness: 1em,
+ double: false,
+ curved: true
+ )
+)
+
+/// Draw a basic cycle chart, describing cyclic steps
+///
+/// ```cexample
+/// let steps = ([Improvise], [Adapt], [Overcome])
+/// let colors = (red, orange, green).map(c => c.lighten(40%))
+///
+/// smartart.cycle.basic(
+/// steps,
+/// step-style: colors,
+/// steps: (max-width: 5cm)
+/// )
+/// ```
+///
+/// === Styling
+/// *Root* `cycle-basic` \
+/// #show-parameter-block("steps.radius", ("number", "length"), [
+/// Corner radius of the steps boxes.], default: 0.2em)
+/// #show-parameter-block("steps.padding", ("number", "length"), [
+/// Inner padding of the steps boxes.], default: 0.6em)
+/// #show-parameter-block("steps.max-width", ("number", "length"), [
+/// Maximum width of the steps boxes.], default: 5em)
+/// #show-parameter-block("steps.shape", ("str", "none"), [
+/// Shape of the steps boxes. One of `"rect"`, `"circle"` or `none`], default: "rect")
+/// #show-parameter-block("steps.fill", ("color", "gradient", "pattern", "none"), [
+/// Fill color of the steps boxes.], default: none)
+/// #show-parameter-block("steps.stroke", ("stroke", "none"), [
+/// Stroke color of the steps boxes.], default: none)
+/// #show-parameter-block("arrows.thickness", ("number", "length"), [
+/// Thickness of arrows.], default: 1em)
+/// #show-parameter-block("arrows.double", ("boolean"), [
+/// Whether arrows are uni- or bi-directional.], default: false)
+/// #show-parameter-block("arrows.curved", ("boolean"), [
+/// Whether arrows are curved or straight.], default: false)
+/// #show-parameter-block("arrows.fill", ("string", "color", "gradient", "pattern", "none"), [
+/// Fill color of the arrows. If set to "steps", the arrows will be filled with a color in between those of the neighboring steps.], default: "steps")
+/// #show-parameter-block("arrows.stroke", ("stroke", "none"), [
+/// Stroke used for the arrows.], default: none)
+///
+/// - steps (array): Array of steps (`` or ``)
+/// - arrow-style (function, array, gradient): Arrow 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 arrow the style at the arrows
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the arrows
+/// index divided by the number of steps as position on the gradient.
+/// If one of stroke or fill is not in the style dictionary, it is taken from the smartarts style.
+/// - step-style (function, array, gradient): Step 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 step the style at the steps
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the steps
+/// index divided by the number of steps as position on the gradient.
+/// If one of stroke or fill is not in the style dictionary, it is taken from the smartarts style.
+/// - equal-width (boolean): If true, all steps will be sized to have the same width
+/// - equal-height (boolean): If true, all steps will be sized to have the same height
+/// - ccw (boolean): If true, steps are laid out counter-clockwise. If false, they're placed clockwise. The center of the cycle is always placed at (0, 0)
+/// - radius (number, length): The radius of the cycle
+/// - offset-angle (angle): Offset of the starting angle
+/// - step-angles (none,angle,array): Angles between the steps.
+/// - none: Steps are spaced evenly.
+/// - angle: Space between the steps, with the last angle (between the last step and the first one) completing the full circle.
+/// - array: An array of angles between the steps. For n steps, the array must contain n-1 angles. The last angle is automatically computed to complete the full circle.
+#let basic(
+ steps,
+ arrow-style: auto,
+ step-style: palette.red,
+ equal-width: false,
+ equal-height: false,
+ ccw: false,
+ radius: 2,
+ offset-angle: 0deg,
+ name: none,
+ step-angles: none,
+ ..style,
+) = {
+ draw.group(name: name, ctx => {
+ draw.anchor("default", (0, 0))
+
+ let style = styles.resolve(
+ ctx.style,
+ merge: style.named(),
+ root: "cycle-basic",
+ base: cycle-basic-default-style,
+ )
+
+ let n-steps = steps.len()
+ let step-style-at = _get-style-at-func(step-style, n-steps)
+ let arrow-style-at = _get-style-at-func(arrow-style, n-steps)
+
+ let (
+ sizes,
+ largest-width,
+ highest-height,
+ ) = _get-steps-sizes(steps, ctx, style, step-style-at)
+
+ let step-angles = if step-angles == none {
+ steps.map(_ => 360deg / n-steps)
+ } else if type(step-angles) == angle {
+ assert(
+ step-angles >= 0deg,
+ message: "step-angles must be positive, use the ccw parameter to change the direction"
+ )
+ assert(
+ (steps.len() - 1) * step-angles <= 360deg,
+ message: "Sum of step angles is greater than 360°"
+ )
+ let angles = (step-angles,) * (steps.len() - 1)
+ angles + (360deg - angles.sum(default: 0deg),)
+ } else if type(step-angles) == array {
+ assert(
+ step-angles.len() == n-steps - 1,
+ message: "There must be one less step angle as there are steps. Expected " + str(n-steps - 1) + ", got " + str(step-angles.len())
+ )
+ for step-angle in step-angles {
+ assert(
+ step-angle >= 0deg,
+ message: "step-angles must be positive, use the ccw parameter to change the direction"
+ )
+ assert(
+ type(step-angle) == angle,
+ message: "All values in step-angles must be angles"
+ )
+ }
+ assert(
+ step-angles.sum(default: 0deg) <= 360deg,
+ message: "Sum of step angles is greater than 360°"
+ )
+ step-angles + (360deg - step-angles.sum(default: 0deg),)
+ } else {
+ panic("step-angles must be an angle, an array or none, got " + repr(type(step-angles)))
+ }
+ if not ccw {
+ step-angles = step-angles.map(x => x * -1)
+ }
+
+ let angle-at = i => (
+ step-angles.slice(0, i)
+ .sum(default: 0deg)
+ + 90deg
+ + offset-angle
+ )
+
+ for (i, step) in steps.enumerate() {
+ let pos = (angle-at(i), radius)
+
+ let step-style = style.steps + step-style-at(i)
+ let padding = resolve-number(ctx, step-style.padding)
+
+ let (w, h) = sizes.at(i)
+ if equal-width {
+ w = largest-width
+ }
+ if equal-height {
+ h = highest-height
+ }
+ let step-name = "step-" + str(i)
+
+ _draw-step(ctx, step, pos, step-style, step-name, w, h)
+ }
+
+ for i in range(n-steps) {
+ let angle = angle-at(i)
+ let arrow-style = style.arrows + arrow-style-at(i)
+ let arrow-stroke = arrow-style.stroke
+ let arrow-fill = arrow-style.fill
+ let arrow-thickness = arrow-style.thickness
+
+ if arrow-fill == "steps" {
+ let s1 = style.steps + step-style-at(i)
+ let s2 = style.steps + step-style-at(i + 1)
+ arrow-fill = gradient.linear(s1.fill, s2.fill).sample(50%)
+ }
+
+ let angle-step = step-angles.at(i)
+ let start-angle = angle + angle-step * 0.2
+ let end-angle = angle + angle-step * 0.8
+
+ let a1 = angle
+ let a2 = angle + angle-step / 2
+ let a3 = angle + angle-step
+ let n(a) = {
+ if a < -180deg {
+ a += 360deg
+ } else if a > 180deg {
+ a -= 360deg
+ }
+ return a
+ }
+ a1 = n(a1)
+ a2 = n(a2)
+ a3 = n(a3)
+ let pts = (
+ (a1, radius),
+ (a2, radius),
+ (a3, radius),
+ )
+ if not ccw {
+ pts = pts.rev()
+ }
+ draw.hide(draw.arc-through(
+ ..pts,
+ name: "arc-" + str(i),
+ ))
+ draw.intersections(
+ "i-" + str(i),
+ "step-" + str(i),
+ "arc-" + str(i),
+ )
+ draw.intersections(
+ "j-" + str(i),
+ "step-" + str(calc.rem(i + 1, n-steps)),
+ "arc-" + str(i),
+ )
+
+ draw.get-ctx(ctx => {
+ let (_, p1) = coordinate.resolve(ctx, "i-" + str(i) + ".0")
+ let (_, p2) = coordinate.resolve(ctx, "j-" + str(i) + ".0")
+ let start-angle = calc.atan2(p1.at(0), p1.at(1))
+ let end-angle = calc.atan2(p2.at(0), p2.at(1))
+ if ccw != (start-angle < end-angle) {
+ if ccw {
+ end-angle += 360deg
+ } else {
+ end-angle -= 360deg
+ }
+ }
+ let angle-d = end-angle - start-angle
+
+ let prev = "step-" + str(i)
+ start-angle += angle-d * 0.1
+ end-angle -= angle-d * 0.1
+ let arrow-name = "arrow-" + str(i)
+
+ let marks = (end: "straight")
+ if arrow-style.double {
+ marks.insert("start", "straight")
+ }
+
+ let arrow-thickness = arrow-thickness
+ if arrow-thickness != none {
+ arrow-thickness = resolve-number(ctx, arrow-thickness)
+ }
+
+ // Curved
+ if arrow-style.curved {
+ // Thin arrow
+ if arrow-thickness == none {
+ draw.arc-through(
+ (start-angle, radius),
+ ((start-angle + end-angle) / 2, radius),
+ (end-angle, radius),
+ stroke: arrow-stroke,
+ mark: marks,
+ name: arrow-name,
+ )
+
+ // Thick arrow
+ } else {
+ _draw-arc-arrow(
+ start-angle,
+ end-angle,
+ radius,
+ arrow-thickness,
+ arrow-fill,
+ arrow-stroke,
+ double: arrow-style.double,
+ name: arrow-name,
+ )
+ }
+
+ // Straight
+ } else {
+ let p1 = (start-angle, radius)
+ let p2 = (end-angle, radius)
+ if arrow-thickness == none {
+ draw.line(
+ p1,
+ p2,
+ stroke: arrow-stroke,
+ mark: marks,
+ name: arrow-name,
+ )
+ } else {
+ _draw-arrow(
+ p1,
+ p2,
+ arrow-thickness,
+ arrow-fill,
+ arrow-stroke,
+ double: arrow-style.double,
+ name: arrow-name,
+ )
+ }
+ }
+ })
+ }
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/smartart/process.typ b/packages/preview/cetz-plot/0.1.4/src/smartart/process.typ
new file mode 100644
index 0000000000..c929a9cad2
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/smartart/process.typ
@@ -0,0 +1,605 @@
+#import "/src/cetz.typ" as cetz: draw, styles, palette, coordinate, util.resolve-number, vector
+#import "common.typ": *
+
+#let process-basic-default-style = (
+ stroke: auto,
+ fill: auto,
+ spacing: 0.2em,
+ steps: (
+ stroke: none,
+ fill: none,
+ radius: 0.2em,
+ padding: 0.6em,
+ max-width: 5em,
+ shape: "rect"
+ ),
+ arrows: (
+ stroke: none,
+ fill: "steps",
+ height: 1em,
+ width: 1.2em,
+ double: false
+ )
+)
+
+#let process-bending-default-style = (
+ stroke: auto,
+ fill: auto,
+ spacing: 0.2em,
+ steps: (
+ stroke: none,
+ fill: none,
+ radius: 0.2em,
+ padding: 0.6em,
+ max-width: 5em,
+ shape: "rect"
+ ),
+ arrows: (
+ stroke: none,
+ fill: "steps",
+ height: 1em,
+ width: 1.2em,
+ double: false
+ ),
+ layout: (
+ max-stride: 3,
+ flow: (ltr, ttb)
+ )
+)
+
+#let process-chevron-default-style = (
+ stroke: auto,
+ fill: auto,
+ spacing: 0.2em,
+ start-cap: ">",
+ middle-cap: ">",
+ end-cap: ">",
+ start-in-cap: false,
+ end-in-cap: false,
+ steps: (
+ stroke: none,
+ fill: none,
+ padding: 0.6em,
+ max-width: 5em,
+ cap-ratio: 50%
+ )
+)
+
+/// Draw a basic process chart, describing sequencial steps
+///
+/// ```cexample
+/// let steps = ([Improvise], [Adapt], [Overcome])
+/// let colors = (red, orange, green).map(c => c.lighten(40%))
+///
+/// smartart.process.basic(
+/// steps,
+/// step-style: colors,
+/// equal-width: true,
+/// steps: (max-width: 8em),
+/// dir: ttb
+/// )
+/// ```
+///
+/// === Styling
+/// *Root* `process-basic` \
+/// #show-parameter-block("spacing", ("number", "length"), [
+/// Gap between steps and arrows.], default: 0.2em)
+/// #show-parameter-block("steps.radius", ("number", "length"), [
+/// Corner radius of the steps boxes.], default: 0.2em)
+/// #show-parameter-block("steps.padding", ("number", "length"), [
+/// Inner padding of the steps boxes.], default: 0.6em)
+/// #show-parameter-block("steps.max-width", ("number", "length"), [
+/// Maximum width of the steps boxes.], default: 5em)
+/// #show-parameter-block("steps.shape", ("str", "none"), [
+/// Shape of the steps boxes. One of `"rect"`, `"circle"` or `none`], default: "rect")
+/// #show-parameter-block("steps.fill", ("color", "gradient", "pattern", "none"), [
+/// Fill color of the steps boxes.], default: none)
+/// #show-parameter-block("steps.stroke", ("stroke", "none"), [
+/// Stroke color of the steps boxes.], default: none)
+/// #show-parameter-block("arrows.width", ("number", "length"), [
+/// Width / length of arrows.], default: 1.2em)
+/// #show-parameter-block("arrows.height", ("number", "length"), [
+/// Height of arrows.], default: 1em)
+/// #show-parameter-block("arrows.double", ("boolean"), [
+/// Whether arrows are uni- or bi-directional.], default: false)
+/// #show-parameter-block("arrows.fill", ("string", "color", "gradient", "pattern", "none"), [
+/// Fill color of the arrows. If set to "steps", the arrows will be filled with a color in between those of the neighboring steps.], default: "steps")
+/// #show-parameter-block("arrows.stroke", ("stroke", "none"), [
+/// Stroke used for the arrows.], default: none)
+///
+/// - steps (array): Array of steps (`` or ``)
+/// - arrow-style (function, array, gradient): Arrow 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 arrow the style at the arrows
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the arrows
+/// index divided by the number of steps as position on the gradient.
+/// If one of stroke or fill is not in the style dictionary, it is taken from the smartarts style.
+/// - step-style (function, array, gradient): Step 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 step the style at the steps
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the steps
+/// index divided by the number of steps as position on the gradient.
+/// If one of stroke or fill is not in the style dictionary, it is taken from the smartarts style.
+/// - equal-width (boolean): If true, all steps will be sized to have the same width
+/// - equal-height (boolean): If true, all steps will be sized to have the same height
+/// - dir (direction): Direction in which the steps are laid out. The first step is always placed at (0, 0)
+#let basic(
+ steps,
+ arrow-style: auto,
+ step-style: palette.red,
+ equal-width: false,
+ equal-height: false,
+ dir: ltr,
+ name: none,
+ ..style
+) = {
+ draw.group(name: name, ctx => {
+ draw.anchor("default", (0, 0))
+
+ let style = styles.resolve(
+ ctx.style,
+ merge: style.named(),
+ root: "process-basic",
+ base: process-basic-default-style,
+ )
+
+ let spacing = resolve-number(ctx, style.spacing)
+
+ let n-steps = steps.len()
+ let step-style-at = _get-style-at-func(step-style, n-steps)
+ let arrow-style-at = _get-style-at-func(arrow-style, n-steps)
+
+ let (
+ sizes,
+ largest-width,
+ highest-height
+ ) = _get-steps-sizes(steps, ctx, style, step-style-at)
+
+ let vertical = dir.axis() == "vertical"
+ let reverse = dir in (rtl, ttb)
+ let adapt-offset(offset, ..args) = {
+ let offset = offset
+ if vertical {
+ if args.pos().len() != 0 {
+ offset = args.pos().first()
+ } else {
+ offset = offset.rev()
+ }
+ }
+ if reverse {
+ offset = offset.map(v => -v)
+ }
+ return offset
+ }
+
+ let (anchor-1, anchor-2) = _dir-to-anchors(dir)
+
+ for (i, step) in steps.enumerate() {
+ let pos = if i == 0 {
+ (0, 0)
+ } else {
+ (
+ rel: adapt-offset((spacing, 0)),
+ to: "arrow-" + str(i - 1) + ".end"
+ )
+ }
+
+ let step-style = style.steps + step-style-at(i)
+ let padding = resolve-number(ctx, step-style.padding)
+
+ let (w, h) = sizes.at(i)
+ if equal-width {
+ w = largest-width
+ }
+ if equal-height {
+ h = highest-height
+ }
+ let step-name = "step-" + str(i)
+
+ _draw-step(
+ ctx, step, pos, step-style, step-name, w, h,
+ dir: if i == 0 {none} else {dir}
+ )
+
+ if i != n-steps - 1 {
+ let arrow-style = style.arrows + arrow-style-at(i)
+ let arrow-stroke = arrow-style.stroke
+ let arrow-fill = arrow-style.fill
+ let arrow-w = resolve-number(ctx, arrow-style.width)
+ let arrow-h = resolve-number(ctx, arrow-style.height)
+
+ if arrow-fill == "steps" {
+ let s1 = style.steps + step-style-at(i)
+ let s2 = style.steps + step-style-at(i + 1)
+ arrow-fill = gradient.linear(s1.fill, s2.fill).sample(50%)
+ }
+
+ let prev = "step-" + str(i)
+ _draw-arrow(
+ (
+ rel: adapt-offset((spacing, 0)),
+ to: prev + "." + anchor-2
+ ),
+ (
+ rel: adapt-offset((spacing + arrow-w, 0)),
+ to: prev + "." + anchor-2
+ ),
+ arrow-h,
+ arrow-fill,
+ arrow-stroke,
+ name: "arrow-" + str(i)
+ )
+ }
+ }
+ })
+}
+
+/// Draw a chevron process chart, describing sequencial steps
+///
+/// ```cexample
+/// let steps = ([Improvise], [Adapt], [Overcome])
+/// let colors = (red, orange, green).map(c => c.lighten(40%))
+///
+/// smartart.process.chevron(
+/// steps,
+/// step-style: colors,
+/// equal-width: true,
+/// steps: (max-width: 8em, cap-ratio: 25%),
+/// dir: btt
+/// )
+/// ```
+///
+/// === Styling
+/// *Root* `process-chevron` \
+/// #show-parameter-block("spacing", ("number", "length"), [
+/// Gap between steps.], default: 0.2em)
+/// #show-parameter-block("start-cap", ("string"), [
+/// Cap at the start of the process (first step). See @@CHEVRON-CAPS for possible values.], default: ">")
+/// #show-parameter-block("mid-cap", ("string"), [
+/// Cap between steps. See @@CHEVRON-CAPS for possible values.], default: ">")
+/// #show-parameter-block("end-cap", ("string"), [
+/// Cap at the end of the process (last step). See @@CHEVRON-CAPS for possible values.], default: ">")
+/// #show-parameter-block("start-in-cap", ("boolean"), [
+/// If true, the content of the first step is shifted inside the start cap (useful with "(" or "<").], default: false)
+/// #show-parameter-block("end-in-cap", ("boolean"), [
+/// If true, the content of the last step is shifted inside the end cap (useful with ")" or ">").], default: false)
+/// #show-parameter-block("steps.padding", ("number", "length"), [
+/// Inner padding of the steps boxes.], default: 0.6em)
+/// #show-parameter-block("steps.max-width", ("number", "length"), [
+/// Maximum width of the steps boxes.], default: 5em)
+/// #show-parameter-block("steps.cap-ratio", ("ratio"), [
+/// Ratio of the caps width relative to the steps heights (or the opposite if laid out vertically).], default: 50%)
+/// #show-parameter-block("steps.fill", ("color", "gradient", "pattern", "none"), [
+/// Fill color of the steps boxes.], default: none)
+/// #show-parameter-block("steps.stroke", ("stroke", "none"), [
+/// Stroke color of the steps boxes.], default: none)
+///
+/// - steps (array): Array of steps (`` or ``)
+/// - step-style (function, array, gradient): Step 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 step the style at the steps
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the steps
+/// index divided by the number of steps as position on the gradient.
+/// If one of stroke or fill is not in the style dictionary, it is taken from the smartarts style.
+/// - equal-length (boolean): If true, all steps will be sized to have the same length (in the layout's direction, the other dimensions always being equal)
+/// - dir (direction): Direction in which the steps are laid out. The first step is always placed at (0, 0)
+#let chevron(
+ steps,
+ step-style: palette.red,
+ equal-length: false,
+ dir: ltr,
+ name: none,
+ ..style
+) = {
+ draw.group(name: name, ctx => {
+ draw.anchor("default", (0, 0))
+
+ let style = styles.resolve(
+ ctx.style,
+ merge: style.named(),
+ root: "process-chevron",
+ base: process-chevron-default-style,
+ )
+
+ let spacing = resolve-number(ctx, style.spacing)
+
+ let n-steps = steps.len()
+ let step-style-at = _get-style-at-func(step-style, n-steps)
+
+ let (
+ sizes,
+ largest-width,
+ highest-height
+ ) = _get-steps-sizes(steps, ctx, style, step-style-at)
+
+ let vertical = dir.axis() == "vertical"
+ let reverse = dir in (rtl, ttb)
+ let adapt-offset(offset, ..args) = {
+ let offset = offset
+ if vertical {
+ if args.pos().len() != 0 {
+ offset = args.pos().first()
+ } else {
+ offset = offset.rev()
+ }
+ }
+ if reverse {
+ offset = offset.map(v => -v)
+ }
+ return offset
+ }
+
+ let (anchor-1, anchor-2) = _dir-to-anchors(dir)
+
+ for (i, step) in steps.enumerate() {
+ let step-style = style.steps + step-style-at(i)
+
+ let step-stroke = step-style.stroke
+ let step-fill = step-style.fill
+ let padding = resolve-number(ctx, step-style.padding)
+
+ let (w, h) = sizes.at(i)
+ if equal-length {
+ if vertical {
+ h = highest-height
+ } else {
+ w = largest-width
+ }
+ }
+ let thickness = if vertical { largest-width } else { highest-height }
+ let cap-height = thickness + padding * 2
+ let cap-width = step-style.cap-ratio / 100% * cap-height
+ let step-name = "step-" + str(i)
+
+ let cap-s = if i == 0 { style.start-cap }
+ else { style.middle-cap }
+ let cap-e = if i == n-steps - 1 { style.end-cap }
+ else { style.middle-cap }
+
+ let pos = if i == 0 {
+ (0, 0)
+ } else {
+ (
+ rel: adapt-offset((
+ spacing + if cap-s != "|" {cap-width} else {0},
+ 0
+ )),
+ to: "step-" + str(i - 1) + ".end"
+ )
+ }
+ let end = (
+ rel: adapt-offset(
+ (w + padding * 2, 0),
+ (0, h + padding * 2)
+ ),
+ to: pos
+ )
+
+ _draw-chevron(
+ pos,
+ end,
+ cap-height,
+ step-fill,
+ step-stroke,
+ cap-s,
+ cap-e,
+ step-style.cap-ratio,
+ i == 0 and style.start-in-cap,
+ (i == n-steps - 1) and style.end-in-cap,
+ name: step-name
+ )
+ _draw-step-content(step, step-name, w * ctx.length)
+ }
+ })
+}
+
+/// Draw a bending process chart, describing sequencial steps in a zigzag layout
+///
+/// ```cexample
+/// let steps = ([A], [B], [C], [D], [E], [F])
+/// let colors = (
+/// red, orange, yellow.mix(green), green, green.mix(blue), blue
+/// ).map(c => c.lighten(40%))
+///
+/// smartart.process.bending(
+/// steps,
+/// step-style: colors,
+/// equal-width: true,
+/// layout: (
+/// flow: (ltr, btt), max-stride: 2
+/// )
+/// )
+/// ```
+///
+/// === Styling
+/// *Root* `process-bending` \
+/// #show-parameter-block("spacing", ("number", "length"), [
+/// Gap between steps and arrows.], default: 0.2em)
+/// #show-parameter-block("steps.radius", ("number", "length"), [
+/// Corner radius of the steps boxes.], default: 0.2em)
+/// #show-parameter-block("steps.padding", ("number", "length"), [
+/// Inner padding of the steps boxes.], default: 0.6em)
+/// #show-parameter-block("steps.max-width", ("number", "length"), [
+/// Maximum width of the steps boxes.], default: 5em)
+/// #show-parameter-block("steps.shape", ("str", "none"), [
+/// Shape of the steps boxes. One of `"rect"`, `"circle"` or `none`], default: "rect")
+/// #show-parameter-block("steps.fill", ("color", "gradient", "pattern", "none"), [
+/// Fill color of the steps boxes.], default: none)
+/// #show-parameter-block("steps.stroke", ("stroke", "none"), [
+/// Stroke color of the steps boxes.], default: none)
+/// #show-parameter-block("arrows.width", ("number", "length"), [
+/// Width / length of arrows.], default: 1.2em)
+/// #show-parameter-block("arrows.height", ("number", "length"), [
+/// Height of arrows.], default: 1em)
+/// #show-parameter-block("arrows.double", ("boolean"), [
+/// Whether arrows are uni- or bi-directional.], default: false)
+/// #show-parameter-block("layout.max-stride", ("number"), [
+/// Maximum number of steps before turning, i.e. making a zigzag.], default: 3)
+/// #show-parameter-block("layout.flow", ("array"), [
+/// Pair of directions on different axes indicating the primary and secondary layout directions.], default: (ltr, ttb))
+/// #show-parameter-block("arrows.fill", ("string", "color", "gradient", "pattern", "none"), [
+/// Fill color of the arrows. If set to "steps", the arrows will be filled with a color in between those of the neighboring steps.], default: "steps")
+/// #show-parameter-block("arrows.stroke", ("stroke", "none"), [
+/// Stroke used for the arrows.], default: none)
+///
+/// - steps (array): Array of steps (`` or ``)
+/// - arrow-style (function, array, gradient): Arrow 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 arrow the style at the arrows
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the arrows
+/// index divided by the number of steps as position on the gradient.
+/// If one of stroke or fill is not in the style dictionary, it is taken from the smartarts style.
+/// - step-style (function, array, gradient): Step 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 step the style at the steps
+/// index modulo the arrays length gets used.
+/// - gradient: A gradient that gets sampled for each data item using the steps
+/// index divided by the number of steps as position on the gradient.
+/// If one of stroke or fill is not in the style dictionary, it is taken from the smartarts style.
+/// - equal-width (boolean): If true, all steps will be sized to have the same width
+/// - equal-height (boolean): If true, all steps will be sized to have the same height
+#let bending(
+ steps,
+ arrow-style: auto,
+ step-style: palette.red,
+ equal-width: false,
+ equal-height: false,
+ name: none,
+ ..style
+) = {
+ draw.group(name: name, ctx => {
+ draw.anchor("default", (0, 0))
+
+ let style = styles.resolve(
+ ctx.style,
+ merge: style.named(),
+ root: "process-bending",
+ base: process-bending-default-style,
+ )
+
+ let n-steps = steps.len()
+ let stride = style.layout.max-stride
+ if stride == none {
+ stride = n-steps
+ }
+ let (flow-primary, flow-secondary) = style.layout.flow
+ assert(
+ flow-primary.axis() != flow-secondary.axis(),
+ message: "Flow axes must be different"
+ )
+ let vertical-first = flow-primary.axis() == "vertical"
+ let primary-reversed = false
+ let secondary-reversed = false
+
+ if vertical-first {
+ primary-reversed = flow-primary == ttb
+ secondary-reversed = flow-secondary == rtl
+ } else {
+ primary-reversed = flow-primary == rtl
+ secondary-reversed = flow-secondary == ttb
+ }
+
+ let spacing = resolve-number(ctx, style.spacing)
+
+ let step-style-at = _get-style-at-func(step-style, n-steps)
+ let arrow-style-at = _get-style-at-func(arrow-style, n-steps)
+
+ let (
+ sizes,
+ largest-width,
+ highest-height
+ ) = _get-steps-sizes(steps, ctx, style, step-style-at)
+
+ let get-step-dir(i) = {
+ // If turning
+ if calc.rem(i, stride) == 0 {
+ return flow-secondary
+ }
+
+ // If "zag"
+ if calc.odd(calc.div-euclid(i, stride)) {
+ return flow-primary.inv()
+ }
+
+ // If "zig"
+ return flow-primary
+ }
+
+ let get-offset(dir, spacing: spacing) = {
+ return (
+ ttb: (0, -1),
+ btt: (0, 1),
+ ltr: (1, 0),
+ rtl: (-1, 0)
+ ).at(_dir-to-str(dir)).map(v => v * spacing)
+ }
+
+ for (i, step) in steps.enumerate() {
+ let dir = get-step-dir(i)
+ let pos = if i == 0 {
+ (0, 0)
+ } else {
+ (
+ rel: get-offset(dir),
+ to: "arrow-" + str(i - 1) + ".end"
+ )
+ }
+
+ let (anchor-1, anchor-2) = _dir-to-anchors(dir)
+ let step-style = style.steps + step-style-at(i)
+ let (w, h) = sizes.at(i)
+ if equal-width {
+ w = largest-width
+ }
+ if equal-height {
+ h = highest-height
+ }
+ let step-name = "step-" + str(i)
+
+ // Draw arrow
+ if i != 0 {
+ let arrow-style = style.arrows + arrow-style-at(i - 1)
+ let arrow-stroke = arrow-style.stroke
+ let arrow-fill = arrow-style.fill
+ let arrow-w = resolve-number(ctx, arrow-style.width)
+ let arrow-h = resolve-number(ctx, arrow-style.height)
+
+ if arrow-fill == "steps" {
+ let s1 = style.steps + step-style-at(i - 1)
+ let s2 = style.steps + step-style-at(i)
+ arrow-fill = gradient.linear(s1.fill, s2.fill).sample(50%)
+ }
+
+ let prev = "step-" + str(i - 1)
+ _draw-arrow(
+ (
+ rel: get-offset(dir),
+ to: prev + "." + anchor-2
+ ),
+ (
+ rel: get-offset(dir, spacing: spacing + arrow-w),
+ to: prev + "." + anchor-2
+ ),
+ arrow-h,
+ arrow-fill,
+ arrow-stroke,
+ name: "arrow-" + str(i - 1)
+ )
+ }
+
+ _draw-step(
+ ctx, step, pos, step-style, step-name, w, h,
+ dir: if i == 0 {none} else {dir}
+ )
+ }
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/src/spine/grid.typ b/packages/preview/cetz-plot/0.1.4/src/spine/grid.typ
new file mode 100644
index 0000000000..d0d818eb65
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/src/spine/grid.typ
@@ -0,0 +1,34 @@
+#import "/src/cetz.typ": vector, draw
+
+// Draw cartesian grid in a rect
+//
+// - proj (function): Axis value->coordinate translation
+// - offset (float): Offset
+// - length (float): Length
+// - direction (vector): Direction vector
+// - ticks (array): List of ticks
+// - style (style): Style dictionary
+// - mode (int): Grid mode 0: no grid, 1 major, 2 minor, 3 both
+#let draw-cartesian(proj, offset, length, direction, ticks, style, mode) = {
+ let direction = vector.norm(direction)
+ let length = direction.map(v => {
+ v * length
+ })
+ let offset = direction.map(v => {
+ v * -offset
+ })
+
+ let major-stroke = style.stroke
+ let minor-stroke = style.minor-stroke
+
+
+ draw.on-layer(style.at("grid-layer", default: 0), {
+ for (value, _, is-major) in ticks {
+ if (if is-major { 1 } else { 2 }).bit-and(mode) != 0 {
+ let origin = vector.add(proj(value), offset)
+ draw.line(origin, vector.add(origin, length),
+ stroke: if is-major { major-stroke } else { minor-stroke })
+ }
+ }
+ })
+}
diff --git a/packages/preview/cetz-plot/0.1.4/typst.toml b/packages/preview/cetz-plot/0.1.4/typst.toml
new file mode 100644
index 0000000000..3a93006375
--- /dev/null
+++ b/packages/preview/cetz-plot/0.1.4/typst.toml
@@ -0,0 +1,15 @@
+[package]
+name = "cetz-plot"
+version = "0.1.4"
+compiler = "0.13.1"
+repository = "https://github.com/cetz-package/cetz-plot"
+entrypoint = "src/lib.typ"
+authors = [
+ "Johannes Wolf ",
+ "fenjalien "
+]
+categories = [ "visualization" ]
+license = "LGPL-3.0-or-later"
+description = "Plotting module for CeTZ."
+keywords = [ "plot", "chart" ]
+exclude = [ "/gallery/*", "manual.pdf", "manual.typ" ]