diff --git a/src/plot.typ b/src/plot.typ index 1aad373..9f1550c 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -13,6 +13,7 @@ #import "/src/plot/errorbar.typ": add-errorbar #import "/src/plot/mark.typ" #import "/src/plot/violin.typ": add-violin +#import "/src/plot/comb.typ": add-comb #import "/src/plot/formats.typ" #import plot-legend: add-legend @@ -459,7 +460,7 @@ } 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, (x, y), d.mark, d.mark-size, size) }) } } diff --git a/src/plot/comb.typ b/src/plot/comb.typ new file mode 100644 index 0000000..3054208 --- /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" + +#let _prepare(self, ctx) = { + let (x-axis, y-axis) = (ctx.x, ctx.y) + self.stroke-paths = self.line-data + .map(((x, y, s, ..)) => { + ( + lines: util.compute-stroke-paths( + ((x, 0), (x,y)), + x-axis, + y-axis + ), + style: s, + ) + + }) + self +} + +#let _fill(self, ctx) = {} + +#let _stroke(self, ctx) = { + for (lines, style) in self.stroke-paths { + for p in lines { + draw.line(..p, fill: none, ..self.style, ..style) + } + } +} + +#let _legend-preview(self) = { + draw.line((0,.5), (1,.5), ..self.style) +} + +#let add-comb( + domain: auto, + mz-key: 0, + intensity-key: 1, + label-key: none, + style-key: none, + style: (:), + mark: none, + mark-size: 0.05, + mark-style: (:), + axes: ("x", "y"), + label: none, + label-padding: none, + annotations: auto, + data +) = { + + let line-data = data.map(d=>( + d.at(mz-key), + d.at(intensity-key), + if style-key != none {d.at(style-key, default: none)} else {style} + )) + + let x-domain = ( + calc.min(..line-data.map(t => t.at(0))), + calc.max(..line-data.map(t => t.at(0))) + ) + + let y-domain = if line-data != none {( + calc.min(..line-data.map(t => t.at(1))), + calc.max(..line-data.map(t => t.at(1))) + )} + + let annotations = if annotations == auto { + if (label-key == none) { + () + } else { + data.filter(it=>it.at(label-key, default: none) != none) + } + } else if annotations == none { + () + } else { + annotations + } + + ((: + type: "comb", + label: label, + data: data, /* Raw data */ + line-data: line-data, /* Transformed 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-fill: _fill, + plot-stroke: _stroke, + // plot-legend-preview: _legend-preview, + mz-key: mz-key, + intensity-key: intensity-key, + label-key: label-key, + width: 0.5, + ),) + + for (x, y, a) in annotations { + annotation.annotate( + draw.content((x,y), [#a], anchor: "south"), + axes: ("x", "y"), + resize: true, + padding: none, + background: false + ) + } + +} + diff --git a/src/plot/mark.typ b/src/plot/mark.typ index 9450d21..97e0986 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,14 @@ } } -#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) - +/// Draw mark shapes for each point in pts +/// - pts (list): Points +/// - all-axes (list): List of axes, order must match pts value order! +#let draw-mark(pts, all-axes, mark, mark-size, plot-size) = { for pt in pts { - draw-mark-shape(pt, mark-size, mark, (:)) + if util.point-in-range(pt, all-axes) { + pt = axes.transform-vec(plot-size, all-axes.at(0), all-axes.at(1), none, pt) + draw-mark-shape(pt, mark-size, mark, (:)) + } } } diff --git a/src/plot/util.typ b/src/plot/util.typ index 5c59c80..9ca5e25 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -370,3 +370,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/tests/plot/annotation/test.typ b/tests/plot/annotation/test.typ index efb04a7..aee5200 100644 --- a/tests/plot/annotation/test.typ +++ b/tests/plot/annotation/test.typ @@ -49,7 +49,7 @@ import draw: * set-style(rect: (stroke: none)) - plot.plot(size: (6, 4), x-tick-step: 1, { + plot.plot(size: (6, 4), x-tick-step: 0.5, { plot.add(domain: (100, 101), calc.sin) plot.annotate(padding: .1, { content( (101.5, 0), [A]) diff --git a/tests/plot/comb/ref.typ b/tests/plot/comb/ref.typ new file mode 100644 index 0000000..91f7951 --- /dev/null +++ b/tests/plot/comb/ref.typ @@ -0,0 +1,162 @@ +#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: "-", + mark-size: 0.2, + data + ) + // plot.add(domain: (0, 100), x=>x, mark: "x") + } + ) +}) diff --git a/tests/plot/comb/testdata.csv b/tests/plot/comb/testdata.csv new file mode 100644 index 0000000..8ca2f60 --- /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 diff --git a/tests/plot/marks/ref/1.png b/tests/plot/marks/ref/1.png index ad6e334..45732ed 100644 Binary files a/tests/plot/marks/ref/1.png and b/tests/plot/marks/ref/1.png differ diff --git a/tests/plot/marks/test.typ b/tests/plot/marks/test.typ index aa584f0..b6677d4 100644 --- a/tests/plot/marks/test.typ +++ b/tests/plot/marks/test.typ @@ -24,3 +24,11 @@ } ) }) + +#test-case({ + import cetz-plot: plot + + plot.plot(size: (5,5), x-min: 1, y-min: 1, x-max: 9, y-max: 9, { + plot.add(domain: (0, 10), samples: 11, x => x, mark: "square") + }) +})