diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..2abb242 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,13 @@ +FROM alpine:3.20 + +RUN apk update +RUN apk add git +RUN apk add curl +RUN apk add just + +RUN apk add --no-cache rust cargo + +ENV PATH="/root/.cargo/bin:$PATH" +RUN apk add openssl openssl-dev +RUN cargo install --locked typst-cli +RUN cargo install --locked --git https://github.com/tingerrr/typst-test --tag ci-semi-stable \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a931a1a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "build": { "dockerfile": "Dockerfile" }, + + "customizations": { + "vscode": { + "extensions": ["myriad-dreamin.tinymist"] + } + }, + + "forwardPorts": [3000] + } \ No newline at end of file diff --git a/manual.typ b/manual.typ index e5b2e1f..be59677 100644 --- a/manual.typ +++ b/manual.typ @@ -54,7 +54,8 @@ 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") { + +#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin", "comb", "legend") { doc-style.parse-show-module("/src/plot/" + m + ".typ") } diff --git a/src/axes.typ b/src/axes.typ index 36a5dad..19cefdb 100644 --- a/src/axes.typ +++ b/src/axes.typ @@ -232,7 +232,8 @@ format: "float" ), mode: auto, base: auto) = ( - min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, mode: mode, base: base + min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, mode: mode, base: base, + kind: "cartesian", ) // Format a tick value @@ -509,44 +510,50 @@ return axis } +// Transform a single value along a cartesian axis +#let transform-cartesian(axis, v) = { + let a = axis.origin + let b = axis.target + + let length = vector.dist(a, b) - axis.inset.sum() + let offset = 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 factor = length / (transform-func(axis.max) - transform-func(axis.min)) + return vector.scale( + vector.norm(vector.sub(b, a)), + (transform-func(v) - transform-func(axis.min)) * factor + offset) +} + // 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 +// - axes (list): List of axes // - 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-vec(axes, vec) = { + let res = (0, 0, 0) + for (dim, axis) in axes.enumerate() { + let v = vec.at(dim, default: 0) - let transform-func(n) = if axis.mode == "log" { - calc.log(calc.max(n, util.float-epsilon), base: axis.base) + if axis.kind == "cartesian" { + res = vector.add(res, transform-cartesian(axis, v)) } else { - n + panic("Unknown axit type " + repr(axis.kind)) } - - 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) + return res } // 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 +// - axes (list): List of axes // - name (string,none): Group name -#let axis-viewport(size, x, y, z, body, name: none) = { +#let axis-viewport(axes, body, name: none) = { draw.group(name: name, (ctx => { let transform = ctx.transform @@ -559,12 +566,12 @@ if "segments" in d { d.segments = d.segments.map(((kind, ..pts)) => { (kind, ..pts.map(pt => { - transform-vec(size, x, y, none, pt) + transform-vec(axes, pt) })) }) } if "pos" in d { - d.pos = transform-vec(size, x, y, none, d.pos) + d.pos = transform-vec(axes, d.pos) } return d }) diff --git a/src/plot.typ b/src/plot.typ index 1aad373..78451f4 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -12,6 +12,7 @@ #import "/src/plot/bar.typ": add-bar #import "/src/plot/errorbar.typ": add-errorbar #import "/src/plot/mark.typ" +#import "/src/plot/comb.typ": add-comb #import "/src/plot/violin.typ": add-violin #import "/src/plot/formats.typ" #import plot-legend: add-legend @@ -28,6 +29,23 @@ return default-plot-style(i) } +/// Add a cartesian axis to a plot +#let add-cartesian-axis(name, origin, target) = { + ((type: "context", fn: (ctx) => { + let axis = ( + name: name, + kind: "cartesian", + origin: origin, + target: target, + min: none, + max: none, + ticks: (step: auto, minor-step: none, format: "float", list: ()) + ) + ctx.axes.insert(name, axis) + return ctx + }),) +} + /// 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:`. @@ -54,8 +72,7 @@ /// #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`). +/// Set the axis domain to keep a fixed aspect ratio by multiplying the other axis domain by the plots aspect ratio. /// This can be useful to force one axis to grow or shrink with another one. /// You can only "lock" two axes of different orientations. /// #example(``` @@ -67,12 +84,6 @@ /// }) /// ```) /// ]) -/// #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.]) @@ -203,45 +214,45 @@ legend-anchor: auto, legend-style: (:), ..options - ) = draw.group(name: name, ctx => { + ) = draw.group(name: name, cetz-ctx => { draw.assert-version(version(0, 3, 1)) - // 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") + // Plot local context + let ctx = ( + origin: (0, 0), + size: size, + axes: (:) + ) + + // Setup default axes + let default-axes = ( + if axis-style in (none, "scientific", "scientific-auto") { + add-cartesian-axis("x", (0,0), (size.at(0),0)) + add-cartesian-axis("x2", (0,size.at(1)), (size.at(0),size.at(1))) + add-cartesian-axis("y", (0,0), (0,size.at(1))) + add-cartesian-axis("y2", (size.at(0),0), (size.at(0),size.at(1))) + } else if axis-style in ("school-book", "left") { + add-cartesian-axis("x", (0,0), (size.at(0),0)) + add-cartesian-axis("y", (0,0), (0,size.at(1))) + } + ) - let x-scale = ((x.max - x.min) / size.at(0)) - let y-scale = ((y.max - y.min) / size.at(1)) + let body = default-axes + body - if y.horizontal { - (x-scale, y-scale) = (y-scale, x-scale) - } + // Create plot context object + let make-ctx(axes, size) = { + assert(axes.len() >= 1, message: "At least one axis must exist") + assert(size.at(0) > 0 and size.at(1) > 0, message: "Plot size must be > 0") - return (x: x, y: y, size: size, x-scale: x-scale, y-scale: y-scale) + return (axes: axes, size: size) } // Setup data viewport - let data-viewport(data, x, y, size, body, name: none) = { + let data-viewport(data, all-axes, body, name: none) = { if body == none or body == () { return } - assert.ne(x.horizontal, y.horizontal, - message: "Data must use one horizontal and one vertical axis!") - - // If y is the horizontal axis, swap x and y - // coordinates by swapping the transformation - // matrix columns. - if y.horizontal { - (x, y) = (y, x) - body = draw.set-ctx(ctx => { - ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) - return ctx - }) + body - } - // Setup the viewport - axes.axis-viewport(size, x, y, none, body, name: name) + axes.axis-viewport(all-axes, body, name: name) } let data = () @@ -256,24 +267,25 @@ anchors.push(cmd) } else if cmd.type == "annotation" { annotations.push(cmd) + } else if cmd.type == "context" { + ctx = (cmd.fn)(ctx) } 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)) - } + assert(name in ctx.axes, message: "Undefined axis " + name) + + let axis = ctx.axes.at(name) + axis.used = true - let axis = axis-dict.at(name) let domain = if i == 0 { d.at("x-domain", default: (none, none)) } else { @@ -284,29 +296,12 @@ axis.max = util.max(axis.max, ..domain) } - axis-dict.at(name) = axis + ctx.axes.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) + ctx.axes = plot-util.setup-axes(cetz-ctx, ctx.axes, options.named(), size) // Prepare styles for i in range(data.len()) { @@ -356,8 +351,7 @@ for i in range(data.len()) { if "axes" not in data.at(i) { continue } - let (x, y) = data.at(i).axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let plot-ctx = make-ctx(data.at(i).axes.map(name => ctx.axes.at(name)), size) if "plot-prepare" in data.at(i) { data.at(i) = (data.at(i).plot-prepare)(data.at(i), plot-ctx) @@ -368,10 +362,9 @@ // Background Annotations for a in annotations.filter(a => a.background) { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let plot-ctx = make-ctx(a.axes.map(name => ctx.axes.at(name)), size) - data-viewport(a, x, y, size, { + data-viewport(a, plot-ctx.axes, { draw.anchor("default", (0, 0)) a.body }) @@ -382,10 +375,9 @@ for d in data { if "axes" not in d { continue } - let (x, y) = d.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let plot-ctx = make-ctx(d.axes.map(name => ctx.axes.at(name)), size) - data-viewport(d, x, y, size, { + data-viewport(d, plot-ctx.axes, { draw.anchor("default", (0, 0)) draw.set-style(..d.style) @@ -412,32 +404,31 @@ 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),) + bottom: ctx.axes.at("x", default: none), + top: ctx.axes.at("x2", default: mirror), + left: ctx.axes.at("y", default: none), + right: ctx.axes.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) + ctx.axes.x, + ctx.axes.y, + x-position: ctx.axes.y.min, + y-position: ctx.axes.x.min) } else if axis-style == "school-book" { axes.school-book( size: size, - axis-dict.x, - axis-dict.y,) + ctx.axes.x, + ctx.axes.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) + let plot-ctx = make-ctx(d.axes.map(name => ctx.axes.at(name)), size) - data-viewport(d, x, y, size, { + data-viewport(d, plot-ctx.axes, { draw.anchor("default", (0, 0)) draw.set-style(..d.style) @@ -451,25 +442,17 @@ if "mark" in d and d.mark != none { draw.scope({ - if y.horizontal { - draw.set-ctx(ctx => { - ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) - return ctx - }) - } - draw.set-style(..d.style, ..d.mark-style) - mark.draw-mark(d.data, x, y, d.mark, d.mark-size, size) + mark.draw-mark(d.data, plot-ctx.axes, d.mark, d.mark-size) }) } } // Foreground Annotations for a in annotations.filter(a => not a.background) { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) + let plot-ctx = make-ctx(a.axes.map(name => ctx.axes.at(name)), size) - data-viewport(a, x, y, size, { + data-viewport(a, plot-ctx.axes, { draw.anchor("default", (0, 0)) a.body }) @@ -477,15 +460,14 @@ // 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 plot-ctx = make-ctx(a.axes.map(name => ctx.axes.at(name)), 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 } + if v == "min" { return plot-ctx.axes.at(i).min } + if v == "max" { return plot-ctx.axes.at(i).max } return v }) - pt = axes.transform-vec(size, x, y, none, pt) + pt = axes.transform-vec(plot-ctx.axes, pt) if pt != none { draw.anchor(a.name, pt) } @@ -496,7 +478,7 @@ 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, + let legend-style = styles.resolve(cetz-ctx.style, base: plot-legend.default-style, merge: legend-style, root: "legend") plot-legend.add-legend-anchors(legend-style, "plot", size) diff --git a/src/plot/bar.typ b/src/plot/bar.typ index 22187a0..212cd30 100644 --- a/src/plot/bar.typ +++ b/src/plot/bar.typ @@ -84,8 +84,7 @@ } #let _draw-rects(filling, self, ctx, ..args) = { - let x-axis = ctx.x - let y-axis = ctx.y + let (x-axis, y-axis, ..) = ctx.axes let bars = () let errors = () @@ -120,7 +119,7 @@ draw.rect((left, y-min), (right, y-max)) if not filling and err != 0 { - let y-whisker-size = self.whisker-size * ctx.x-scale + let y-whisker-size = self.whisker-size //* ctx.x-scale FIXME draw-errorbar(((left + right) / 2, y-max), 0, err, 0, y-whisker-size / 2, self.style + self.error-style) } diff --git a/src/plot/comb.typ b/src/plot/comb.typ new file mode 100644 index 0000000..879faad --- /dev/null +++ b/src/plot/comb.typ @@ -0,0 +1,115 @@ +#import "/src/cetz.typ": draw, vector +#import "util.typ" +#import "line.typ" +#import "annotation.typ" + +// Internal: This function takes the line-data (a sanitized input) and calculates +// which points should be visible, and if they are partially clipped, recalcuates +// positions +#let _prepare(self, ctx) = { + self.stroke-paths = self.line-data + .map( + ((x, y, style, ..)) => {( + lines: util.compute-stroke-paths( ((x, 0), (x,y)), ctx.axes), + style: style, + )}) + self +} + +// Visible: Draw the lines using the pre-calculated stroke paths from earlier. +// The overall style is first applied, and then overriden +#let _stroke(self, ctx) = { + for (lines, style) in self.stroke-paths { + for p in lines { + draw.line(..p, fill: none, ..self.style, ..style) + } + } +} + +/// Add a comb plot to a plot environment. +/// +/// Must be called from the body of a `plot(..)` command. +/// +/// #example(``` +/// let points = ( +/// (0,4), +/// (1,2), +/// (2,5, (stroke: red)), +/// (3,1), +/// (4,3) +/// ) +/// plot.plot(size: (12, 3), y-min: 0, x-inset: 0.5, y-inset: (0,0.5), { +/// plot.add-comb( +/// points, +/// style-key: 2 // Indicate which key sfor tyle overrides (optional) +/// ) +/// }) +/// ```, vertical: true) +/// +/// - data (array,dictionary): Array of 2D data points (and optionally a style +/// override) +/// - x-key (int,string): Key to use for retrieving an x-value from +/// a single data entry. This value gets passed to the `.at(...)` +/// function of a data item. Resulting value must be a number. +/// - y-key (int,string): Key to use for retrieving a +/// y-value. Resulting value must be a number. +/// - style (style): Style to use, can be used with a `palette` function +/// - style-key (int,string,none): Key to use for retrieving a `style` +/// with which to override the current style. Resulting value must +/// be either a `style` or `none` +/// - mark (string): Mark symbol to place at each distinct value of the +/// graph. Uses the `mark` style key of `style` for drawing. +/// - mark-size (float): Mark size in cavas units +/// - mark-style (style): Style override for marks. +/// - axes (axes): Name of the axes to use for plotting. Reversing the axes +/// means rotating the plot by 90 degrees +/// - label (none, content): The name of the category to be shown in the legend. +#let add-comb( + x-key: 0, + y-key: 1, + style-key: none, + style: (:), + mark: none, + mark-size: 0.05, + mark-style: (:), + axes: ("x", "y"), + label: none, + data +) = { + + // Convert the input data into a sanitized format so that it isn't needed + // to store those keys in the element dictionary + let line-data = data.map(d=>( + x: d.at(x-key), + y: d.at(y-key), + style: if style-key != none {d.at(style-key, default: none)} else {style}, + )) + + // Calculate the domains along both axes + let x-domain = ( + calc.min(..line-data.map(t => t.x)), + calc.max(..line-data.map(t => t.x)) + ) + + let y-domain = if line-data != none {( + calc.min(..line-data.map(t => t.y)), + calc.max(..line-data.map(t => t.y)) + )} + + ((: + type: "comb", // internal type indentifier + label: label, + data: line-data.map(((x, y,..))=>(x,y)), /* X-Y data */ + line-data: line-data, /* formatted data */ + axes: axes, + x-domain: x-domain, + y-domain: y-domain, + style: style, + mark: mark, + mark-size: mark-size, + mark-style: mark-style, + plot-prepare: _prepare, + plot-stroke: _stroke, + ),) + +} diff --git a/src/plot/contour.typ b/src/plot/contour.typ index 7c42cf2..b03ed25 100644 --- a/src/plot/contour.typ +++ b/src/plot/contour.typ @@ -206,13 +206,11 @@ // Prepare line data #let _prepare(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - self.contours = self.contours.map(c => { - c.stroke-paths = util.compute-stroke-paths(c.line-data, x, y) + c.stroke-paths = util.compute-stroke-paths(c.line-data, ctx.axes) if self.fill { - c.fill-paths = util.compute-fill-paths(c.line-data, x, y) + c.fill-paths = util.compute-fill-paths(c.line-data, ctx.axes) } return c }) diff --git a/src/plot/errorbar.typ b/src/plot/errorbar.typ index e8ec68c..7417b65 100644 --- a/src/plot/errorbar.typ +++ b/src/plot/errorbar.typ @@ -47,8 +47,8 @@ } #let _stroke(self, ctx) = { - let x-whisker-size = self.whisker-size * ctx.y-scale - let y-whisker-size = self.whisker-size * ctx.x-scale + let x-whisker-size = self.whisker-size //* ctx.y-scale FIXME + let y-whisker-size = self.whisker-size //* ctx.x-scale FIXME draw-errorbar((self.x, self.y), self.x-error, self.y-error, diff --git a/src/plot/line.typ b/src/plot/line.typ index a509d18..c709281 100644 --- a/src/plot/line.typ +++ b/src/plot/line.typ @@ -83,17 +83,15 @@ // 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) + self.stroke-paths = util.compute-stroke-paths(self.line-data, ctx.axes) // 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) + self.fill-paths = util.compute-fill-paths(self.line-data, ctx.axes) } return self @@ -101,8 +99,6 @@ // 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) } @@ -110,7 +106,7 @@ // Fill line data #let _fill(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) + let (x, y, ..) = ctx.axes if self.hypograph { fill-segments-to(self.fill-paths, y.min) @@ -295,8 +291,9 @@ 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, y, ..) = ctx.axes + let (x-min, x-max) = (x.min, x.max) + let (y-min, y-max) = (y.min, y.max) let x-min = if min == auto { x-min } else { min } let x-max = if max == auto { x-max } else { max } @@ -357,8 +354,9 @@ 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 (x, y, ..) = ctx.axes + let (x-min, x-max) = (x.min, x.max) + let (y-min, y-max) = (y.min, y.max) let y-min = if min == auto { y-min } else { min } let y-max = if max == auto { y-max } else { max } @@ -455,16 +453,14 @@ )} 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), + a: util.compute-stroke-paths(self.line-data.a, ctx.axes), + b: util.compute-stroke-paths(self.line-data.b, ctx.axes), ) // Generate fill paths - self.fill-paths = util.compute-fill-paths(self.line-data.a + self.line-data.b.rev(), x, y) + self.fill-paths = util.compute-fill-paths(self.line-data.a + self.line-data.b.rev(), ctx.axes) return self } diff --git a/src/plot/mark.typ b/src/plot/mark.typ index 9450d21..f085305 100644 --- a/src/plot/mark.typ +++ b/src/plot/mark.typ @@ -1,5 +1,6 @@ #import "/src/cetz.typ": draw #import "/src/axes.typ" +#import "/src/plot/util.typ" // Draw mark at point with size #let draw-mark-shape(pt, size, mark, style) = { @@ -34,12 +35,11 @@ } } -#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) - +#let draw-mark(pts, all-axes, mark, mark-size) = { for pt in pts { - draw-mark-shape(pt, mark-size, mark, (:)) + if util.point-in-range(pt, all-axes) { + pt = axes.transform-vec(all-axes, pt) + draw-mark-shape(pt, mark-size, mark, (:)) + } } } diff --git a/src/plot/util.typ b/src/plot/util.typ index 5c59c80..70b5057 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -1,5 +1,6 @@ #import "/src/cetz.typ" #import cetz.util: bezier +#import cetz: vector /// Clip line-strip in rect /// @@ -173,20 +174,20 @@ /// Compute clipped stroke paths /// /// - points (array): X/Y data points -/// - x (axis): X-Axis -/// - y (axis): Y-Axis +/// - axes (list): List of axes /// -> array List of stroke paths -#let compute-stroke-paths(points, x, y) = { +#let compute-stroke-paths(points, axes) = { + let (x, y, ..) = axes 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 +/// - axes (list): List of axes /// -> array List of fill paths -#let compute-fill-paths(points, x, y) = { +#let compute-fill-paths(points, axes) = { + let (x, y, ..) = axes clipped-paths(points, (x.min, y.min), (x.max, y.max), fill: true) } @@ -289,16 +290,22 @@ } for (name, axis) in axis-dict { + let used = axis.at("used", default: false) + if not "ticks" in axis { axis.ticks = () } - axis.label = get-axis-option(name, "label", $#name$) + axis.label = get-axis-option(name, "label", if used { $#name$ } else { axis.at("label", default: none) }) // 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 == none { + axis.min = 0 + axis.ticks.step = none + axis.ticks.minor-step = none + axis.ticks.format = none + } + if axis.max == none { axis.max = axis.min } if axis.min == axis.max { axis.min -= 1; axis.max += 1 } @@ -344,15 +351,13 @@ 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.") + assert(axis.kind == "cartesian" and other.kind == "cartesian", + message: "Bothe axes must be cartesian.") + + let dir = vector.sub(axis.target, axis.origin) + let other-dir = vector.sub(other.target, other.origin) + let ratio = vector.len(dir) / vector.len(other-dir) - 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 @@ -370,3 +375,18 @@ return axis-dict } + +/// Tests if point pt is contained in the +/// axis interval of each axis in axes +/// - pt (list): Data array +/// - axes (list): List of axes +#let point-in-range(pt, axes) = { + for i in range(0, axes.len()) { + let a = axes.at(i) + let v = pt.at(i) + if v < a.min or v > a.max { + return false + } + } + return true +} diff --git a/src/plot/violin.typ b/src/plot/violin.typ index 5da01e9..8d0c00c 100644 --- a/src/plot/violin.typ +++ b/src/plot/violin.typ @@ -20,7 +20,7 @@ path = path.map(((x,y)) => (2 * violin.x-position - x,y)) } - let stroke-paths = util.compute-stroke-paths(path, ctx.x, ctx.y) + let stroke-paths = util.compute-stroke-paths(path, ctx.axes) for p in stroke-paths{ let args = arguments(..p, closed: self.side == "both") diff --git a/tests/axes/log-mode/ref/1.png b/tests/axes/log-mode/ref/1.png index 409c2c3..53fa4ea 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/chart/boxwhisker/ref/1.png b/tests/chart/boxwhisker/ref/1.png index 07b311a..c71898b 100644 Binary files a/tests/chart/boxwhisker/ref/1.png and b/tests/chart/boxwhisker/ref/1.png differ diff --git a/tests/chart/ref/1.png b/tests/chart/ref/1.png index a9aab7b..236c26c 100644 Binary files a/tests/chart/ref/1.png and b/tests/chart/ref/1.png differ diff --git a/tests/plot/annotation/ref/1.png b/tests/plot/annotation/ref/1.png index 77cd005..3509931 100644 Binary files a/tests/plot/annotation/ref/1.png and b/tests/plot/annotation/ref/1.png differ diff --git a/tests/plot/bar/ref/1.png b/tests/plot/bar/ref/1.png index 307100d..532feb7 100644 Binary files a/tests/plot/bar/ref/1.png and b/tests/plot/bar/ref/1.png differ diff --git a/tests/plot/boxwhisker/ref/1.png b/tests/plot/boxwhisker/ref/1.png index 2f1e16e..acc6e59 100644 Binary files a/tests/plot/boxwhisker/ref/1.png and b/tests/plot/boxwhisker/ref/1.png differ diff --git a/tests/plot/broken-axes/ref/1.png b/tests/plot/broken-axes/ref/1.png index fcd2e11..27057d4 100644 Binary files a/tests/plot/broken-axes/ref/1.png and b/tests/plot/broken-axes/ref/1.png differ diff --git a/tests/plot/comb/ref/1.png b/tests/plot/comb/ref/1.png new file mode 100644 index 0000000..cfe80d3 Binary files /dev/null and b/tests/plot/comb/ref/1.png differ diff --git a/tests/plot/comb/test.typ b/tests/plot/comb/test.typ new file mode 100644 index 0000000..36dc317 --- /dev/null +++ b/tests/plot/comb/test.typ @@ -0,0 +1,179 @@ +#set page(width: auto, height: auto) +#import "/src/cetz.typ": * +#import "/src/lib.typ": * +#import "/tests/helper.typ": * + +#let data = csv("testdata.csv").map( + ((x, y,..))=>{ + ( + float(x), + float(y), + if x in ("41",) { + (stroke: (paint: red)) + } else if x in ("93",){ + (stroke: (paint: blue)) + }, + ) + } +) + += General case +- Input data is an array of the form (mz, int, ..) +- keys are not explicitly set. +- X, Y ranges not set + +#test-case({ + plot.plot( + size: (10,6), + // y-max: 100, + // x-min: 0, x-max: 175, + { + plot.add-comb( + label: "Linalool, 70eV", + // style-key: 2, + // style: (stroke: (paint: black)), + data + ) + } + ) +}) + + += With domain set +- General case, but X Y domains are defined explicitly and without mistake + +#table( + columns: 3, + ..(for i in range(0, 9) { + let (x,y) = (calc.div-euclid(i, 3),calc.rem-euclid(i, 3)) + (table.cell( x: x, y: 3-y, test-case({ + plot.plot( + x-label: none, y-label: none, + x-tick-step: none, y-tick-step: none, + size: (3,3), + x-min: x * 50, x-max: (x+1) * 50, + y-min: y * 33, y-max: (y+1) * 33, + { + plot.add-comb( + data + ) + } + ) + })),) + }) +) + += With uniform style +Applying the same style to the whole series + +#test-case({ + plot.plot( + size: (10,6), + // y-max: 100, + // x-min: 0, x-max: 175, + { + plot.add-comb( + label: "Linalool, 70eV", + // style-key: 2, + style: (stroke: (paint: black, dash: "dashed")), + data + ) + } + ) +}) + += With uniform style and individual style +Applying the same style across a whole series, except for some for which it is defined explicitly\ as a field set by `style-key` + +#test-case({ + plot.plot( + size: (10,6), + // y-max: 100, + // x-min: 0, x-max: 175, + { + plot.add-comb( + label: "Linalool, 70eV", + style-key: 2, + style: (stroke: (paint: black, dash: "dashed")), + data + ) + } + ) +}) + += With Marks +Uniform marks across the series + +#test-case({ + plot.plot( + size: (10,6), + // y-max: 100, + x-min: 35, x-max: 45, + { + plot.add-comb( + label: "Linalool, 70eV", + mark: "-", + mark-size: 0.2, + data + ) + // plot.add(domain: (0, 100), x=>x, mark: "x") + } + ) +}) + += Axis swap +// Test pending upstream +#test-case({ + plot.plot( + size: (10,6), + y-max: 0, y-min: 180, + // x-min: 35, x-max: 45, + { + plot.add-comb( + axes: ("y", "x"), + label: "Linalool, 70eV", + // mark: "-", + mark-size: 0.2, + data + ) + // plot.add(domain: (0, 100), x=>x, mark: "x") + } + ) +}) + += Logarithym +// Test pending upstream +#test-case({ + plot.plot( + size: (10,6), + // x-min: 35, x-max: 45, + y-max: 100, + y-mode: "log", y-tick-step: 1, y-base: 10, y-format: "sci", y-minor-tick-step: 1, + { + plot.add-comb( + label: "Linalool, 70eV", + mark: "o", + mark-size: 0.2, + data + ) + // plot.add(domain: (0, 100), x=>x, mark: "x") + } + ) +}) + +#test-case({ + plot.plot( + size: (10,6), + x-min: 10, x-max: 1000, + y-max: 100, y-tick-step: 20, + x-mode: "log", x-tick-step: 1, x-base: 10, x-format: "sci", + { + plot.add-comb( + label: "Linalool, 70eV", + mark: "x", + mark-size: 0.2, + data + ) + } + ) +}) \ No newline at end of file diff --git a/tests/plot/comb/testdata.csv b/tests/plot/comb/testdata.csv new file mode 100644 index 0000000..1dbca28 --- /dev/null +++ b/tests/plot/comb/testdata.csv @@ -0,0 +1,57 @@ +15,2,20 +18,1.15,12 +26,1.04,10 +27,21.16,212 +28,3.01,30 +29,10.19,102 +31,2.24,22 +39,22.09,221 +40,4.97,50 +41,63.43,634 +42,5.86,59 +43,58.84,588 +44,1.63,16 +45,1.58,16 +51,3.62,36 +52,1.45,15 +53,13.41,134 +54,2.5,25 +55,46.66,467 +56,9.02,90 +57,3.48,35 +58,2.51,25 +59,3.42,34 +65,3.06,31 +66,1.17,12 +67,15.88,159 +68,12.86,129 +69,40.64,406 +70,5,50 +71,99.99,999 +72,7.53,75 +77,4.19,42 +79,8.08,81 +80,26.17,262 +81,10.85,109 +82,5.73,57 +83,16.15,162 +84,4.3,43 +85,1.36,14 +86,1.62,16 +91,4.76,48 +92,11.35,114 +93,61.4,614 +94,8.79,88 +95,2.72,27 +96,7.47,75 +97,1.96,20 +105,3.07,31 +107,5.78,58 +108,1.34,13 +109,5.02,50 +111,2.96,30 +121,19.93,199 +122,1.59,16 +136,8.77,88 +137,1.04,10 +139,2.18,22 \ No newline at end of file diff --git a/tests/plot/contour/ref/1.png b/tests/plot/contour/ref/1.png index adad8ab..f7d0c39 100644 Binary files a/tests/plot/contour/ref/1.png and b/tests/plot/contour/ref/1.png differ diff --git a/tests/plot/equal-axis/ref/1.png b/tests/plot/equal-axis/ref/1.png index 7f4196f..283fd6d 100644 Binary files a/tests/plot/equal-axis/ref/1.png and b/tests/plot/equal-axis/ref/1.png differ diff --git a/tests/plot/equal-axis/test.typ b/tests/plot/equal-axis/test.typ index 7c5f83f..a9b0a8f 100644 --- a/tests/plot/equal-axis/test.typ +++ b/tests/plot/equal-axis/test.typ @@ -10,9 +10,10 @@ x-tick-step: none, y-tick-step: none, x-equal: "y", - a-equal: "b", - b-horizontal: true, + b-equal: "a", { + plot.add-cartesian-axis("a", (0,0), (6,0)) + plot.add-cartesian-axis("b", (0,0), (0,3)) plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t))) plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t)), axes: ("a", "b")) @@ -26,9 +27,10 @@ x-tick-step: none, y-tick-step: none, x-equal: "y", - a-equal: "b", - b-horizontal: true, + b-equal: "a", { + plot.add-cartesian-axis("a", (0,0), (3,0)) + plot.add-cartesian-axis("b", (0,0), (0,6)) plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t))) plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t)), axes: ("a", "b")) diff --git a/tests/plot/format/ref/1.png b/tests/plot/format/ref/1.png index 2b5a0ec..a7a2925 100644 Binary files a/tests/plot/format/ref/1.png and b/tests/plot/format/ref/1.png differ diff --git a/tests/plot/grid/ref/1.png b/tests/plot/grid/ref/1.png index 550e9d5..49e291b 100644 Binary files a/tests/plot/grid/ref/1.png and b/tests/plot/grid/ref/1.png differ diff --git a/tests/plot/line/line-type/ref/1.png b/tests/plot/line/line-type/ref/1.png index 7814191..127004b 100644 Binary files a/tests/plot/line/line-type/ref/1.png and b/tests/plot/line/line-type/ref/1.png differ diff --git a/tests/plot/line/linearization/ref/1.png b/tests/plot/line/linearization/ref/1.png index ea8c022..b52049f 100644 Binary files a/tests/plot/line/linearization/ref/1.png and b/tests/plot/line/linearization/ref/1.png differ diff --git a/tests/plot/line/mark/ref/1.png b/tests/plot/line/mark/ref/1.png index f4e7ca6..8c476bc 100644 Binary files a/tests/plot/line/mark/ref/1.png and b/tests/plot/line/mark/ref/1.png differ diff --git a/tests/plot/line/spline/ref/1.png b/tests/plot/line/spline/ref/1.png index ba6e25b..e3d75f3 100644 Binary files a/tests/plot/line/spline/ref/1.png and b/tests/plot/line/spline/ref/1.png differ diff --git a/tests/plot/marks/ref/1.png b/tests/plot/marks/ref/1.png index ad6e334..3fd1fc0 100644 Binary files a/tests/plot/marks/ref/1.png and b/tests/plot/marks/ref/1.png differ diff --git a/tests/plot/mirror-axes/ref/1.png b/tests/plot/mirror-axes/ref/1.png index 4e7e062..94b2cc2 100644 Binary files a/tests/plot/mirror-axes/ref/1.png and b/tests/plot/mirror-axes/ref/1.png differ diff --git a/tests/plot/parametric/ref/1.png b/tests/plot/parametric/ref/1.png index 18557d3..02bb203 100644 Binary files a/tests/plot/parametric/ref/1.png and b/tests/plot/parametric/ref/1.png differ diff --git a/tests/plot/ref/1.png b/tests/plot/ref/1.png index 2cfb1b8..9f3c5e8 100644 Binary files a/tests/plot/ref/1.png and b/tests/plot/ref/1.png differ diff --git a/tests/plot/reverse-axis/ref/1.png b/tests/plot/reverse-axis/ref/1.png index d29c1b6..f959809 100644 Binary files a/tests/plot/reverse-axis/ref/1.png and b/tests/plot/reverse-axis/ref/1.png differ diff --git a/tests/plot/test.typ b/tests/plot/test.typ index 9ea7b68..4cf0944 100644 --- a/tests/plot/test.typ +++ b/tests/plot/test.typ @@ -136,6 +136,10 @@ yb-min: -1.5, yb-max: .5, yt-min: -.5, yt-max: 1.5, { + plot.add-cartesian-axis("xl", (0, 0), (4, 0)) + plot.add-cartesian-axis("xr", (0, 4), (4, 4)) + plot.add-cartesian-axis("yt", (0, 0), (0, 4)) + plot.add-cartesian-axis("yb", (4, 0), (4, 4)) plot.add(circle-data) plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) @@ -155,6 +159,10 @@ yb-min: -1.75, yb-max: .25, yt-min: -.25, yt-max: 1.75, { + plot.add-cartesian-axis("xl", (0, 0), (4, 0)) + plot.add-cartesian-axis("xr", (0, 4), (4, 4)) + plot.add-cartesian-axis("yt", (0, 0), (0, 4)) + plot.add-cartesian-axis("yb", (4, 0), (4, 4)) plot.add(circle-data) plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) diff --git a/tests/plot/vertical/ref/1.png b/tests/plot/vertical/ref/1.png index f034306..93e0cbd 100644 Binary files a/tests/plot/vertical/ref/1.png and b/tests/plot/vertical/ref/1.png differ diff --git a/tests/plot/vertical/test.typ b/tests/plot/vertical/test.typ index ffe61c2..cb566fc 100644 --- a/tests/plot/vertical/test.typ +++ b/tests/plot/vertical/test.typ @@ -6,8 +6,7 @@ #test-case({ import draw: * - plot.plot(size: (10, 10), - { + plot.plot(size: (10, 10), { plot.add(domain: (0, 4*calc.pi), calc.sin, axes: ("y", "x"), mark: "+") }) }) @@ -15,8 +14,7 @@ #test-case({ import draw: * - plot.plot(size: (10, 10), - { + plot.plot(size: (10, 10), { plot.add-contour(x-domain: (0, 4), y-domain: (-2, 2), (x, y) => x - .5 * y, op: ">=", z: 2, axes: ("y", "x"), fill: true) }) diff --git a/tests/plot/violin/ref/1.png b/tests/plot/violin/ref/1.png index 63e1210..acfc488 100644 Binary files a/tests/plot/violin/ref/1.png and b/tests/plot/violin/ref/1.png differ