diff --git a/README.md b/README.md index 76630c2..08788a5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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.4.2! +CeTZ-Plot requires CeTZ version ≥ 0.5.0! ## Examples @@ -63,8 +63,8 @@ For information, see the [manual (stable)](https://github.com/cetz-package/cetz- To use this package, simply add the following code to your document: ``` -#import "@preview/cetz:0.4.2" -#import "@preview/cetz-plot:0.1.3": plot, chart +#import "@preview/cetz:0.5.2" +#import "@preview/cetz-plot:0.1.4": plot, chart #cetz.canvas({ // Your plot/chart code goes here diff --git a/doc/style.typ b/doc/style.typ index 21de664..ff37880 100644 --- a/doc/style.typ +++ b/doc/style.typ @@ -1,14 +1,14 @@ #import "/src/lib.typ" #import "@preview/tidy:0.4.3" -#import "@preview/t4t:0.3.2": is +#import "@preview/t4t:0.3.2": is as is_ #let show-function(fn, style-args) = { [ #heading(fn.name, level: style-args.first-heading-level + 1) #label(style-args.label-prefix + fn.name + "()") ] - let description = if is.sequence(fn.description) { + let description = if is_.sequence(fn.description) { fn.description.children } else { (fn.description,) diff --git a/gallery/barchart.png b/gallery/barchart.png index 572cf00..4a412e2 100644 Binary files a/gallery/barchart.png and b/gallery/barchart.png differ diff --git a/gallery/barchart.typ b/gallery/barchart.typ index 88fec52..5369350 100644 --- a/gallery/barchart.typ +++ b/gallery/barchart.typ @@ -1,5 +1,5 @@ -#import "@preview/cetz:0.4.2": canvas, draw -#import "@preview/cetz-plot:0.1.3": chart +#import "@preview/cetz:0.5.2": canvas, draw +#import "@preview/cetz-plot:0.1.4": chart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/bending.typ b/gallery/bending.typ index 309ef95..312b4a8 100644 --- a/gallery/bending.typ +++ b/gallery/bending.typ @@ -1,4 +1,4 @@ -#import "@preview/cetz:0.4.2" as cetz: draw +#import "@preview/cetz:0.5.2" as cetz: draw #import "/src/lib.typ": smartart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/chevron.png b/gallery/chevron.png index 5b95d3e..22f569f 100644 Binary files a/gallery/chevron.png and b/gallery/chevron.png differ diff --git a/gallery/chevron.typ b/gallery/chevron.typ index 4956b38..44ea9c0 100644 --- a/gallery/chevron.typ +++ b/gallery/chevron.typ @@ -1,4 +1,4 @@ -#import "@preview/cetz:0.4.2" as cetz: draw +#import "@preview/cetz:0.5.2" as cetz: draw #import "/src/lib.typ": smartart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/circular.typ b/gallery/circular.typ index aea10c0..f80f5c3 100644 --- a/gallery/circular.typ +++ b/gallery/circular.typ @@ -1,4 +1,4 @@ -#import "@preview/cetz:0.4.2" as cetz: draw +#import "@preview/cetz:0.5.2" as cetz: draw #import "/src/lib.typ": smartart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/cycles.typ b/gallery/cycles.typ index c07c5b0..2d1c6fd 100644 --- a/gallery/cycles.typ +++ b/gallery/cycles.typ @@ -1,4 +1,4 @@ -#import "@preview/cetz:0.4.2": canvas, draw +#import "@preview/cetz:0.5.2": canvas, draw #import "/src/lib.typ": smartart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/line.png b/gallery/line.png index c8babfa..7e2e0dd 100644 Binary files a/gallery/line.png and b/gallery/line.png differ diff --git a/gallery/line.typ b/gallery/line.typ index 83b9297..2edb5d2 100644 --- a/gallery/line.typ +++ b/gallery/line.typ @@ -1,5 +1,5 @@ -#import "@preview/cetz:0.4.2": canvas, draw -#import "@preview/cetz-plot:0.1.3": plot +#import "@preview/cetz:0.5.2": canvas, draw +#import "@preview/cetz-plot:0.1.4": plot #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/normal-dist.png b/gallery/normal-dist.png new file mode 100644 index 0000000..87b2406 Binary files /dev/null and b/gallery/normal-dist.png differ diff --git a/gallery/normal-dist.typ b/gallery/normal-dist.typ new file mode 100644 index 0000000..14c2a40 --- /dev/null +++ b/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/gallery/piechart.typ b/gallery/piechart.typ index a391825..819d6c1 100644 --- a/gallery/piechart.typ +++ b/gallery/piechart.typ @@ -1,5 +1,5 @@ -#import "@preview/cetz:0.4.2" -#import "@preview/cetz-plot:0.1.3": chart +#import "@preview/cetz:0.5.2" +#import "@preview/cetz-plot:0.1.4": chart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/process.typ b/gallery/process.typ index 294c46b..c522223 100644 --- a/gallery/process.typ +++ b/gallery/process.typ @@ -1,4 +1,4 @@ -#import "@preview/cetz:0.4.2" as cetz: draw +#import "@preview/cetz:0.5.2" as cetz: draw #import "/src/lib.typ": smartart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/pyramid.typ b/gallery/pyramid.typ index 89fef14..bc0e75a 100644 --- a/gallery/pyramid.typ +++ b/gallery/pyramid.typ @@ -1,5 +1,5 @@ -#import "@preview/cetz:0.4.2" -#import "@preview/cetz-plot:0.1.3": chart +#import "@preview/cetz:0.5.2" +#import "@preview/cetz-plot:0.1.4": chart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/radarchart.png b/gallery/radarchart.png new file mode 100644 index 0000000..15bf3e7 Binary files /dev/null and b/gallery/radarchart.png differ diff --git a/gallery/radarchart.typ b/gallery/radarchart.typ new file mode 100644 index 0000000..99b3521 --- /dev/null +++ b/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/manual.pdf b/manual.pdf index 9af75e1..08cf6d6 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index 03d0ffa..f7b5c58 100644 --- a/manual.typ +++ b/manual.typ @@ -21,8 +21,8 @@ #set terms(indent: 1em) #set par(justify: true) #set heading(numbering: (..num) => if num.pos().len() < 4 { - numbering("1.1", ..num) - }) + numbering("1.1", ..num) +}) #show link: set text(blue) // Outline @@ -42,8 +42,8 @@ CeTZ-Plot is a simple plotting library for use with CeTZ. This is the minimal starting point: #pad(left: 1em)[```typ -#import "@preview/cetz:0.4.2" -#import "@preview/cetz-plot:0.1.3" +#import "@preview/cetz:0.5.2" +#import "@preview/cetz-plot:0.1.4" #cetz.canvas({ import cetz.draw: * import cetz-plot: * @@ -58,7 +58,17 @@ module imported into the namespace. #doc-style.parse-show-module("/src/plot.typ") -#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin", "legend") { +#for m in ( + "line", + "bar", + "boxwhisker", + "contour", + "errorbar", + "annotation", + "formats", + "violin", + "legend", +) { doc-style.parse-show-module("/src/plot/" + m + ".typ") } @@ -87,7 +97,14 @@ plot.plot(size: (5, 4), axis-style: "school-book", y-tick-step: none, { = Chart #doc-style.parse-show-module("/src/chart.typ") -#for m in ("barchart", "boxwhisker", "columnchart", "piechart", "pyramid") { +#for m in ( + "barchart", + "boxwhisker", + "columnchart", + "piechart", + "radarchart", + "pyramid", +) { doc-style.parse-show-module("/src/chart/" + m + ".typ") } diff --git a/src/cetz.typ b/src/cetz.typ index 624a277..8a95d8f 100644 --- a/src/cetz.typ +++ b/src/cetz.typ @@ -1,2 +1,2 @@ // Import cetz into the root scope. Import cetz by importing this file only! -#import "@preview/cetz:0.4.2": * +#import "@preview/cetz:0.5.2": * diff --git a/src/chart.typ b/src/chart.typ index e8f11a2..c9344a7 100644 --- a/src/chart.typ +++ b/src/chart.typ @@ -2,4 +2,5 @@ #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/pyramid.typ": pyramid, pyramid-default-style \ No newline at end of file +#import "chart/radarchart.typ": radarchart, radarchart-default-style +#import "chart/pyramid.typ": pyramid, pyramid-default-style diff --git a/src/chart/piechart.typ b/src/chart/piechart.typ index ba41a0a..e32fcac 100644 --- a/src/chart/piechart.typ +++ b/src/chart/piechart.typ @@ -1,6 +1,4 @@ #import "/src/cetz.typ": draw, styles, palette, util, vector, intersection -#import util: circle-arclen - #import "/src/plot/legend.typ" // Piechart Label Kind @@ -344,6 +342,10 @@ 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) diff --git a/src/chart/radarchart.typ b/src/chart/radarchart.typ new file mode 100644 index 0000000..3dbbdb4 --- /dev/null +++ b/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/src/plot.typ b/src/plot.typ index 7f50209..f0f55ca 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -202,7 +202,7 @@ legend-style: (:), ..options ) = draw.group(name: name, ctx => { - draw.assert-version(version(0, 4, 2)) + draw.assert-version(version(0, 5, 0), max: version(0, 6, 0)) // Create plot context object let make-ctx(x, y, size) = { @@ -233,7 +233,14 @@ if y.horizontal { (x, y) = (y, x) body = draw.set-ctx(ctx => { - ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) + 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 } @@ -451,7 +458,14 @@ draw.scope({ if y.horizontal { draw.set-ctx(ctx => { - ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) + 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 }) } diff --git a/src/plot/contour.typ b/src/plot/contour.typ index ad00ebf..94b6b56 100644 --- a/src/plot/contour.typ +++ b/src/plot/contour.typ @@ -14,9 +14,17 @@ // 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, contour-limit: 50) = { +#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), @@ -58,6 +66,22 @@ // 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 { @@ -74,28 +98,12 @@ return none } - // Get case (0 to 15) - let get-case(tl, tr, bl, br) = { - int(tl) * 8 + int(tr) * 4 + int(br) * 2 + int(bl) - } - - let lerp(a, b) = { - if a == b { return a } - else if a == none { return 1 } - else if b == none { return 0 } - return (offset - a) / (b - a) - } - - // List of all found contours - let contours = () - - let segments = () - for y in range(-1, n-rows) { - for x in range(-1, n-cols) { + 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) + let tr = get-bin(x + 1, y) + let bl = get-bin(x, y + 1) + let br = get-bin(x + 1, y + 1) // Corner data // @@ -105,9 +113,9 @@ // | | // 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) + 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 // @@ -204,25 +212,39 @@ 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.contours = self.contours.map(c => { + self.stroke-contours = self.stroke-contours.map(c => { c.stroke-paths = util.compute-stroke-paths(c.line-data, x, y) - - if self.fill { - c.fill-paths = util.compute-fill-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.contours { + for c in self.stroke-contours { for p in c.stroke-paths { draw.line(..p, fill: none, close: p.first() == p.last()) } @@ -232,7 +254,7 @@ // Fill line data #let _fill(self, ctx) = { if not self.fill { return } - for c in self.contours { + for c in self.fill-contours { for p in c.fill-paths { draw.line(..p, stroke: none, close: p.first() == p.last()) } @@ -310,26 +332,26 @@ let (y-min, y-max) = y-domain let dy = (y-max - y-min) / (data.len() - 1) - let contours = () + let stroke-contours = () + let fill-contours = () let z = if type(z) == array { z } else { (z,) } for z in z { - for contour in find-contours(data, z, op: op, interpolate: interpolate, contour-limit: limit) { - let line-data = contour.map(pt => { - (pt.at(0) * dx + x-min, - pt.at(1) * dy + y-min) - }) - - contours.push(( - z: z, - line-data: line-data, - )) + 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, - contours: contours, + stroke-contours: stroke-contours, + fill-contours: fill-contours, axes: axes, x-domain: x-domain, y-domain: y-domain, diff --git a/src/plot/legend.typ b/src/plot/legend.typ index aec0e80..94838d4 100644 --- a/src/plot/legend.typ +++ b/src/plot/legend.typ @@ -170,7 +170,7 @@ // Draw item preview let draw-preview = if preview == auto { draw-generic-preview } else { preview } - scope({ + 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) }) diff --git a/src/smartart/cycle.typ b/src/smartart/cycle.typ index 580c37a..0ebbff0 100644 --- a/src/smartart/cycle.typ +++ b/src/smartart/cycle.typ @@ -82,6 +82,10 @@ /// - 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, @@ -92,7 +96,8 @@ radius: 2, offset-angle: 0deg, name: none, - ..style + step-angles: none, + ..style, ) = { draw.group(name: name, ctx => { draw.anchor("default", (0, 0)) @@ -111,17 +116,58 @@ let ( sizes, largest-width, - highest-height + highest-height, ) = _get-steps-sizes(steps, ctx, style, step-style-at) - let angle-step = 360deg / n-steps + 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 { - angle-step *= -1 + 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 angle = angle-step * i + 90deg + offset-angle - let pos = (angle, radius) + let pos = (angle-at(i), radius) let step-style = style.steps + step-style-at(i) let padding = resolve-number(ctx, step-style.padding) @@ -139,8 +185,7 @@ } for i in range(n-steps) { - let angle = angle-step * i + 90deg + offset-angle - + 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 @@ -152,6 +197,7 @@ 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 @@ -179,17 +225,17 @@ } draw.hide(draw.arc-through( ..pts, - name: "arc-" + str(i) + name: "arc-" + str(i), )) draw.intersections( "i-" + str(i), "step-" + str(i), - "arc-" + str(i) + "arc-" + str(i), ) draw.intersections( "j-" + str(i), "step-" + str(calc.rem(i + 1, n-steps)), - "arc-" + str(i) + "arc-" + str(i), ) draw.get-ctx(ctx => { @@ -231,9 +277,9 @@ (end-angle, radius), stroke: arrow-stroke, mark: marks, - name: arrow-name + name: arrow-name, ) - + // Thick arrow } else { _draw-arc-arrow( @@ -244,29 +290,31 @@ arrow-fill, arrow-stroke, double: arrow-style.double, - name: arrow-name + name: arrow-name, ) } - + // Straight } else { let p1 = (start-angle, radius) let p2 = (end-angle, radius) if arrow-thickness == none { draw.line( - p1, p2, + p1, + p2, stroke: arrow-stroke, mark: marks, - name: arrow-name + name: arrow-name, ) } else { _draw-arrow( - p1, p2, + p1, + p2, arrow-thickness, arrow-fill, arrow-stroke, double: arrow-style.double, - name: arrow-name + name: arrow-name, ) } } diff --git a/tests/axes/log-mode/ref/1.png b/tests/axes/log-mode/ref/1.png index 8572759..54a96f1 100644 Binary files a/tests/axes/log-mode/ref/1.png and b/tests/axes/log-mode/ref/1.png differ diff --git a/tests/axes/self/ref/1.png b/tests/axes/self/ref/1.png index 09a285a..8046fb8 100644 Binary files a/tests/axes/self/ref/1.png and b/tests/axes/self/ref/1.png differ diff --git a/tests/chart/boxwhisker/ref/1.png b/tests/chart/boxwhisker/ref/1.png index 4490aa0..8c3a649 100644 Binary files a/tests/chart/boxwhisker/ref/1.png and b/tests/chart/boxwhisker/ref/1.png differ diff --git a/tests/chart/piechart/ref/1.png b/tests/chart/piechart/ref/1.png index 2d05a1d..61d565a 100644 Binary files a/tests/chart/piechart/ref/1.png and b/tests/chart/piechart/ref/1.png differ diff --git a/tests/chart/radarchart/ref/1.png b/tests/chart/radarchart/ref/1.png new file mode 100644 index 0000000..f169c78 Binary files /dev/null and b/tests/chart/radarchart/ref/1.png differ diff --git a/tests/chart/radarchart/test.typ b/tests/chart/radarchart/test.typ new file mode 100644 index 0000000..cbbf1fc --- /dev/null +++ b/tests/chart/radarchart/test.typ @@ -0,0 +1,37 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * +#import "/tests/helper.typ": * + +#let labels = ( + [A], + [B], + [C], + [D], + [E], +) + +#test-case({ + chart.radarchart( + labels, + (0.3, 1, 0.3, 0.8, 0.8), + ) +}) + +#test-case({ + chart.radarchart( + labels, + ( + (0.3, 1, 0.3, 0.8, 0.8), + (0.9, 0.3, 0.9, 0.5, 0.5), + (0.6, 0.5, 0, 0.5, 0.1), + ), + radius: 3, + web-label-offset: 0.6, + web-ticks: 3, + data-style: ( + blue.transparentize(30%), + red.transparentize(30%), + green.transparentize(30%), + ), + ) +}) diff --git a/tests/chart/self/ref/1.png b/tests/chart/self/ref/1.png index bf399c4..e772e48 100644 Binary files a/tests/chart/self/ref/1.png and b/tests/chart/self/ref/1.png differ diff --git a/tests/plot/contour/ref/1.png b/tests/plot/contour/ref/1.png index 3966296..0651bdf 100644 Binary files a/tests/plot/contour/ref/1.png and b/tests/plot/contour/ref/1.png differ diff --git a/tests/plot/legend/ref/1.png b/tests/plot/legend/ref/1.png index 0fc8e61..e58a342 100644 Binary files a/tests/plot/legend/ref/1.png and b/tests/plot/legend/ref/1.png differ diff --git a/tests/plot/vertical/ref/1.png b/tests/plot/vertical/ref/1.png index e13e158..47f5906 100644 Binary files a/tests/plot/vertical/ref/1.png and b/tests/plot/vertical/ref/1.png differ diff --git a/tests/plot/violin/ref/1.png b/tests/plot/violin/ref/1.png index 1f49fcb..7efb9c7 100644 Binary files a/tests/plot/violin/ref/1.png and b/tests/plot/violin/ref/1.png differ diff --git a/tests/smartart/cycle/ref/1.png b/tests/smartart/cycle/ref/1.png index bc3be81..c39e35f 100644 Binary files a/tests/smartart/cycle/ref/1.png and b/tests/smartart/cycle/ref/1.png differ diff --git a/tests/smartart/cycle/ref/2.png b/tests/smartart/cycle/ref/2.png index 3661020..d9255bc 100644 Binary files a/tests/smartart/cycle/ref/2.png and b/tests/smartart/cycle/ref/2.png differ diff --git a/tests/smartart/cycle/ref/3.png b/tests/smartart/cycle/ref/3.png index 1469c76..8aa456a 100644 Binary files a/tests/smartart/cycle/ref/3.png and b/tests/smartart/cycle/ref/3.png differ diff --git a/tests/smartart/cycle/test.typ b/tests/smartart/cycle/test.typ index faccfde..b5fead5 100644 --- a/tests/smartart/cycle/test.typ +++ b/tests/smartart/cycle/test.typ @@ -219,4 +219,23 @@ (angle: 0deg, ccw: true), (angle: 15deg, ccw: true), (angle: -20deg, ccw: true), +)) + +#let steps = ([A], [B], [C]) + +#test-case(args => { + defaults() + smartart.cycle.basic( + steps, + step-style: none, + step-angles: args.step-angles, + ccw: args.ccw + ) +}, args: ( + (step-angles: none, ccw: false), + (step-angles: none, ccw: true), + (step-angles: 60deg, ccw: false), + (step-angles: 60deg, ccw: true), + (step-angles: (60deg, 120deg), ccw: false), + (step-angles: (60deg, 120deg), ccw: true), )) \ No newline at end of file diff --git a/typst.toml b/typst.toml index 6058a2d..3a93006 100644 --- a/typst.toml +++ b/typst.toml @@ -1,6 +1,6 @@ [package] name = "cetz-plot" -version = "0.1.3" +version = "0.1.4" compiler = "0.13.1" repository = "https://github.com/cetz-package/cetz-plot" entrypoint = "src/lib.typ"