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/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/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..a34eaea 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/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/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/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/legend/ref/1.png b/tests/plot/legend/ref/1.png index 0fc8e61..a5ec75f 100644 Binary files a/tests/plot/legend/ref/1.png and b/tests/plot/legend/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..40ee222 100644 Binary files a/tests/smartart/cycle/ref/3.png and b/tests/smartart/cycle/ref/3.png differ 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"