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/manual.pdf b/manual.pdf index 9af75e1..9b72c18 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index 03d0ffa..b49c2da 100644 --- a/manual.typ +++ b/manual.typ @@ -57,6 +57,7 @@ module imported into the namespace. = Plot #doc-style.parse-show-module("/src/plot.typ") +#doc-style.parse-show-module("/src/plot/groupplots.typ") #for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin", "legend") { doc-style.parse-show-module("/src/plot/" + m + ".typ") diff --git a/src/lib.typ b/src/lib.typ index f52981b..6ca9b88 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -4,3 +4,7 @@ #import "/src/plot.typ" #import "/src/chart.typ" #import "/src/smartart.typ" + +// Expose groupplots +#import "/src/plot/groupplots.typ": groupplots + diff --git a/src/plot.typ b/src/plot.typ index 7f50209..f45ae9d 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -184,6 +184,20 @@ /// ]) /// - 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 +/// - title (none, string, content): Plot title. +/// - title-style (dictionary): Style for the title. +/// #show-parameter-block("font", ("string"), default: "\"bold\"", [ +/// Font style of the title. +/// ]) +/// #show-parameter-block("padding", ("number"), default: ".2cm", [ +/// Padding between title and plot. +/// ]) +/// #show-parameter-block("anchor", ("string"), default: "\"south\"", [ +/// Anchor of the title content. +/// ]) +/// #show-parameter-block("offset", ("pair"), default: "(0,0)", [ +/// Offset vector for fine-tuning the title position. +/// ]) /// - 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 @@ -197,6 +211,8 @@ plot-style: default-plot-style, mark-style: default-mark-style, fill-below: true, + title: none, + title-style: (:), legend: auto, legend-anchor: auto, legend-style: (:), @@ -428,6 +444,25 @@ axis-dict.y,) } + // Draw Title + if title != none { + // Styles + let title-style = styles.resolve(ctx.style, + base: ( + font: "bold", + padding: .2cm, + anchor: "south", + offset: (0, 0) + ), merge: title-style, root: "plot.title") + + let (w, h) = size + let padding = util.resolve-number(ctx, title-style.padding) + let pos = (w / 2, h + padding) + pos = vector.add(pos, title-style.offset) + + draw.content(pos, title, anchor: title-style.anchor) + } + // Stroke + Mark data for d in data { if "axes" not in d { continue } diff --git a/src/plot/groupplots.typ b/src/plot/groupplots.typ new file mode 100644 index 0000000..77d129a --- /dev/null +++ b/src/plot/groupplots.typ @@ -0,0 +1,109 @@ + +import "/src/cetz.typ" + +/// Create a group of plots. +/// +/// This function allows arranging multiple plots in a grid layout. +/// It takes a variable number of arguments, alternating between options (dictionary) and plot content (body). +/// +/// == Options +/// +/// - columns (int): Number of columns in the grid. +/// - size (array): Size of each plot `(width, height)`. +/// - rows (auto, int): Number of rows. If `auto`, calculated from number of plots and columns. +/// - horizontal-sep (float): Horizontal separation between plots. +/// - vertical-sep (float): Vertical separation between plots. +/// - title (none, string, content): Global title for the group of plots. +/// - sublabels (none, string): Numbering pattern for sublabels (e.g., "(a)"). +/// - ..options (any): Default options passed to every plot. +/// - ..cont (any): Alternating plot options (dictionary) and plot content (body). +/// +/// == Example +/// +/// ```typst +/// groupplots( +/// 3, (4, 4), +/// horizontal-sep: 1.5, vertical-sep: 1.5, +/// (title: "Plot 1"), { plot.add(...) }, +/// (title: "Plot 2"), { plot.add(...) } +/// ) +/// ``` +#let groupplots( + columns, + size, + rows: auto, + horizontal-sep: 1, + vertical-sep: 1, + title: none, + sublabels: none, + group-style: (:), + ..options +) = { + import "/src/plot.typ" as plot_mod + import "/src/cetz.typ" + + let default-params = options.named() + default-params.insert("size", size) + + let cont = options.pos() + + assert(cont.len() > 0, message: "No plots provided to groupplots.") + assert(calc.rem(cont.len(), 2) == 0, message: "groupplots expects alternating options and body arguments.") + + let plot-items = () + let plot-titles = () // Keep track of titles for sublabels matching if needed, though plot handles its own title now. + let has-xlabel = false + + for i in range(0, cont.len(), step: 2) { + let plot-args = cont.at(i) + let plot-content = cont.at(i + 1) + + if type(plot-args) != dictionary { + if plot-args == () { + plot-args = (:) + } else { + panic("Expected a dictionary or empty array for plot arguments at index " + str(i) + ", found " + type(plot-args)) + } + } + + let merged-args = default-params + plot-args + + if "x-label" in merged-args and merged-args.at("x-label") != none { + has-xlabel = true + } + + if plot-content != none { + plot-items.push(plot_mod.plot(..merged-args, plot-content)) + } + } + + let n = plot-items.len() + let grid-rows = if rows == auto { calc.ceil(n / columns) } else { rows } + let (plot-width, plot-height) = size + + cetz.draw.group(name: "groupplots", { + if title != none { + let total-width = columns * plot-width + (columns - 1) * horizontal-sep + cetz.draw.content((total-width / 2, plot-height + 1), text(weight: "bold", title), anchor: "south") + } + + for j in range(0, n) { + let col = calc.rem(j, columns) + let row = calc.floor(j / columns) + + let ox = col * (plot-width + horizontal-sep) + let oy = -(row * (plot-height + vertical-sep)) + + cetz.draw.group({ + cetz.draw.set-origin((ox, oy)) + plot-items.at(j) + + // Sublabels + if sublabels != none { + let sublabel-y = if has-xlabel { -1.0 } else { -0.5 } + cetz.draw.content((plot-width / 2, sublabel-y), numbering(sublabels, j + 1), anchor: "north") + } + }) + } + }) +} diff --git a/tests/plot/groupplots/test.typ b/tests/plot/groupplots/test.typ new file mode 100644 index 0000000..7a52ef6 --- /dev/null +++ b/tests/plot/groupplots/test.typ @@ -0,0 +1,63 @@ +#set page(width: auto, height: auto) +#import "/src/cetz.typ": * +#import "/src/lib.typ": * +#import "/tests/helper.typ": * + +// Test default groupplots +#test-case({ + groupplots( + 2, (3, 3), + horizontal-sep: 1, vertical-sep: 1, + title: "Global Title", // global title + sublabels: "(a)", + + // Plot 1: with title + (title: "Plot 1"), { plot.add(((0,0), (1,1))) }, + + // Plot 2: with title and custom style + (title: "Plot 2", title-style: (offset: (0, 0.5))), { plot.add(((0,0), (1,1))) }, + + // Plot 3: empty options (should use defaults) + (), { plot.add(((0,0), (1,1))) }, + + // Plot 4: with options but no title + (x-label: "X Label"), { plot.add(((0,0), (1,1))) } + ) +}) + +// Test regular plot with title +#test-case({ + plot.plot( + size: (3,3), + title: "Regular Plot Title", + title-style: (padding: 0.5cm), + { plot.add(((0,0), (1,1))) } + ) +}) + +// Test user example from cetz_groupplots.typ +#test-case({ + let factor = -10 + groupplots( + 3, // 3 columns + (4, 4), // plot size + horizontal-sep: 1.5, vertical-sep: 1.5, + title: "Global Title", + sublabels: "(a)", + x-tick-step: 1, + y-tick-step: 1, + (x-tick-step: 0.5, title: "Plot 1"), + { plot.add(((0,0), (1,1), (2,0.5), (4,3))) }, + (), + { plot.add(((0,0), (1,1), (2,0.5), (4,-3))) }, + (x-tick-step: 2), + { plot.add(((0,0), (1,1), (2,0.5), (4,3))) + plot.add(((0,0), (1,1), (2,0.5), (4,-3))) }, + (x-tick-step: 4, y-tick-step: 10000), + { plot.add(((0,0), (1,10000), (2,5000), (4,30000))) }, + (x-tick-step: 4, y-tick-step: 1*calc.abs(factor)), + { plot.add(((0,0), (1,1*factor), (2,0.5*factor), (4,3*factor))) }, + (x2-tick-step: 4, y2-tick-step: 100000), + { plot.add(axes: ("x2", "y2"), ((0,0), (1,100000), (2,50000), (4,300000))) } + ) +}) diff --git a/typst.toml b/typst.toml index 6058a2d..8a54848 100644 --- a/typst.toml +++ b/typst.toml @@ -1,6 +1,6 @@ [package] -name = "cetz-plot" -version = "0.1.3" +name = "cetz-plot-fork" +version = "0.1.4" compiler = "0.13.1" repository = "https://github.com/cetz-package/cetz-plot" entrypoint = "src/lib.typ"