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/plot.typ b/src/plot.typ index 27ef51a..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 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/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