Skip to content

Commit d9bf4f2

Browse files
Add comb plot element (#84)
2 parents cf6c9b5 + 6f67e87 commit d9bf4f2

6 files changed

Lines changed: 354 additions & 1 deletion

File tree

manual.typ

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ module imported into the namespace.
5454
= Plot
5555

5656
#doc-style.parse-show-module("/src/plot.typ")
57-
#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin", "legend") {
57+
58+
#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin", "comb", "legend") {
5859
doc-style.parse-show-module("/src/plot/" + m + ".typ")
5960
}
6061

src/plot.typ

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#import "/src/plot/bar.typ": add-bar
1313
#import "/src/plot/errorbar.typ": add-errorbar
1414
#import "/src/plot/mark.typ"
15+
#import "/src/plot/comb.typ": add-comb
1516
#import "/src/plot/violin.typ": add-violin
1617
#import "/src/plot/formats.typ"
1718
#import plot-legend: add-legend

src/plot/comb.typ

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#import "/src/cetz.typ": draw, vector
2+
#import "util.typ"
3+
#import "line.typ"
4+
#import "annotation.typ"
5+
6+
// Internal: This function takes the line-data (a sanitized input) and calculates
7+
// which points should be visible, and if they are partially clipped, recalcuates
8+
// positions
9+
#let _prepare(self, ctx) = {
10+
self.stroke-paths = self.line-data
11+
.map(
12+
((x, y, style, ..)) => {(
13+
lines: util.compute-stroke-paths( ((x, 0), (x,y)), ctx.axes),
14+
style: style,
15+
)})
16+
self
17+
}
18+
19+
// Visible: Draw the lines using the pre-calculated stroke paths from earlier.
20+
// The overall style is first applied, and then overriden
21+
#let _stroke(self, ctx) = {
22+
for (lines, style) in self.stroke-paths {
23+
for p in lines {
24+
draw.line(..p, fill: none, ..self.style, ..style)
25+
}
26+
}
27+
}
28+
29+
/// Add a comb plot to a plot environment.
30+
///
31+
/// Must be called from the body of a `plot(..)` command.
32+
///
33+
/// #example(```
34+
/// let points = (
35+
/// (0,4),
36+
/// (1,2),
37+
/// (2,5, (stroke: red)),
38+
/// (3,1),
39+
/// (4,3)
40+
/// )
41+
/// plot.plot(size: (12, 3), y-min: 0, x-inset: 0.5, y-inset: (0,0.5), {
42+
/// plot.add-comb(
43+
/// points,
44+
/// style-key: 2 // Indicate which key sfor tyle overrides (optional)
45+
/// )
46+
/// })
47+
/// ```, vertical: true)
48+
///
49+
/// - data (array,dictionary): Array of 2D data points (and optionally a style
50+
/// override)
51+
/// - x-key (int,string): Key to use for retrieving an x-value from
52+
/// a single data entry. This value gets passed to the `.at(...)`
53+
/// function of a data item. Resulting value must be a number.
54+
/// - y-key (int,string): Key to use for retrieving a
55+
/// y-value. Resulting value must be a number.
56+
/// - style (style): Style to use, can be used with a `palette` function
57+
/// - style-key (int,string,none): Key to use for retrieving a `style`
58+
/// with which to override the current style. Resulting value must
59+
/// be either a `style` or `none`
60+
/// - mark (string): Mark symbol to place at each distinct value of the
61+
/// graph. Uses the `mark` style key of `style` for drawing.
62+
/// - mark-size (float): Mark size in cavas units
63+
/// - mark-style (style): Style override for marks.
64+
/// - axes (axes): Name of the axes to use for plotting. Reversing the axes
65+
/// means rotating the plot by 90 degrees
66+
/// - label (none, content): The name of the category to be shown in the legend.
67+
#let add-comb(
68+
x-key: 0,
69+
y-key: 1,
70+
style-key: none,
71+
style: (:),
72+
mark: none,
73+
mark-size: 0.05,
74+
mark-style: (:),
75+
axes: ("x", "y"),
76+
label: none,
77+
data
78+
) = {
79+
80+
// Convert the input data into a sanitized format so that it isn't needed
81+
// to store those keys in the element dictionary
82+
let line-data = data.map(d=>(
83+
x: d.at(x-key),
84+
y: d.at(y-key),
85+
style: if style-key != none {d.at(style-key, default: none)} else {style},
86+
))
87+
88+
// Calculate the domains along both axes
89+
let x-domain = (
90+
calc.min(..line-data.map(t => t.x)),
91+
calc.max(..line-data.map(t => t.x))
92+
)
93+
94+
let y-domain = if line-data != none {(
95+
calc.min(..line-data.map(t => t.y)),
96+
calc.max(..line-data.map(t => t.y))
97+
)}
98+
99+
((:
100+
type: "comb", // internal type indentifier
101+
label: label,
102+
data: line-data.map(((x, y,..))=>(x,y)), /* X-Y data */
103+
line-data: line-data, /* formatted data */
104+
axes: axes,
105+
x-domain: x-domain,
106+
y-domain: y-domain,
107+
style: style,
108+
mark: mark,
109+
mark-size: mark-size,
110+
mark-style: mark-style,
111+
plot-prepare: _prepare,
112+
plot-stroke: _stroke,
113+
),)
114+
115+
}

tests/plot/comb/ref/1.png

217 KB
Loading

tests/plot/comb/test.typ

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#set page(width: auto, height: auto)
2+
#import "/src/cetz.typ": *
3+
#import "/src/lib.typ": *
4+
#import "/tests/helper.typ": *
5+
6+
#let data = csv("testdata.csv").map(
7+
((x, y,..))=>{
8+
(
9+
float(x),
10+
float(y),
11+
if x in ("41",) {
12+
(stroke: (paint: red))
13+
} else if x in ("93",){
14+
(stroke: (paint: blue))
15+
},
16+
)
17+
}
18+
)
19+
20+
= General case
21+
- Input data is an array of the form (mz, int, ..)
22+
- keys are not explicitly set.
23+
- X, Y ranges not set
24+
25+
#test-case({
26+
plot.plot(
27+
size: (10,6),
28+
// y-max: 100,
29+
// x-min: 0, x-max: 175,
30+
{
31+
plot.add-comb(
32+
label: "Linalool, 70eV",
33+
// style-key: 2,
34+
// style: (stroke: (paint: black)),
35+
data
36+
)
37+
}
38+
)
39+
})
40+
41+
42+
= With domain set
43+
- General case, but X Y domains are defined explicitly and without mistake
44+
45+
#table(
46+
columns: 3,
47+
..(for i in range(0, 9) {
48+
let (x,y) = (calc.div-euclid(i, 3),calc.rem-euclid(i, 3))
49+
(table.cell( x: x, y: 3-y, test-case({
50+
plot.plot(
51+
x-label: none, y-label: none,
52+
x-tick-step: none, y-tick-step: none,
53+
size: (3,3),
54+
x-min: x * 50, x-max: (x+1) * 50,
55+
y-min: y * 33, y-max: (y+1) * 33,
56+
{
57+
plot.add-comb(
58+
data
59+
)
60+
}
61+
)
62+
})),)
63+
})
64+
)
65+
66+
= With uniform style
67+
Applying the same style to the whole series
68+
69+
#test-case({
70+
plot.plot(
71+
size: (10,6),
72+
// y-max: 100,
73+
// x-min: 0, x-max: 175,
74+
{
75+
plot.add-comb(
76+
label: "Linalool, 70eV",
77+
// style-key: 2,
78+
style: (stroke: (paint: black, dash: "dashed")),
79+
data
80+
)
81+
}
82+
)
83+
})
84+
85+
= With uniform style and individual style
86+
Applying the same style across a whole series, except for some for which it is defined explicitly\ as a field set by `style-key`
87+
88+
#test-case({
89+
plot.plot(
90+
size: (10,6),
91+
// y-max: 100,
92+
// x-min: 0, x-max: 175,
93+
{
94+
plot.add-comb(
95+
label: "Linalool, 70eV",
96+
style-key: 2,
97+
style: (stroke: (paint: black, dash: "dashed")),
98+
data
99+
)
100+
}
101+
)
102+
})
103+
104+
= With Marks
105+
Uniform marks across the series
106+
107+
#test-case({
108+
plot.plot(
109+
size: (10,6),
110+
// y-max: 100,
111+
x-min: 35, x-max: 45,
112+
{
113+
plot.add-comb(
114+
label: "Linalool, 70eV",
115+
mark: "-",
116+
mark-size: 0.2,
117+
data
118+
)
119+
// plot.add(domain: (0, 100), x=>x, mark: "x")
120+
}
121+
)
122+
})
123+
124+
= Axis swap
125+
// Test pending upstream
126+
#test-case({
127+
plot.plot(
128+
size: (10,6),
129+
y-max: 0, y-min: 180,
130+
// x-min: 35, x-max: 45,
131+
{
132+
plot.add-comb(
133+
axes: ("y", "x"),
134+
label: "Linalool, 70eV",
135+
// mark: "-",
136+
mark-size: 0.2,
137+
data
138+
)
139+
// plot.add(domain: (0, 100), x=>x, mark: "x")
140+
}
141+
)
142+
})
143+
144+
= Logarithym
145+
// Test pending upstream
146+
#test-case({
147+
plot.plot(
148+
size: (10,6),
149+
// x-min: 35, x-max: 45,
150+
y-max: 100,
151+
y-mode: "log", y-tick-step: 1, y-base: 10, y-format: "sci", y-minor-tick-step: 1,
152+
{
153+
plot.add-comb(
154+
label: "Linalool, 70eV",
155+
mark: "o",
156+
mark-size: 0.2,
157+
data
158+
)
159+
// plot.add(domain: (0, 100), x=>x, mark: "x")
160+
}
161+
)
162+
})
163+
164+
#test-case({
165+
plot.plot(
166+
size: (10,6),
167+
x-min: 10, x-max: 1000,
168+
y-max: 100, y-tick-step: 20,
169+
x-mode: "log", x-tick-step: 1, x-base: 10, x-format: "sci",
170+
{
171+
plot.add-comb(
172+
label: "Linalool, 70eV",
173+
mark: "x",
174+
mark-size: 0.2,
175+
data
176+
)
177+
}
178+
)
179+
})

tests/plot/comb/testdata.csv

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
15,2,20
2+
18,1.15,12
3+
26,1.04,10
4+
27,21.16,212
5+
28,3.01,30
6+
29,10.19,102
7+
31,2.24,22
8+
39,22.09,221
9+
40,4.97,50
10+
41,63.43,634
11+
42,5.86,59
12+
43,58.84,588
13+
44,1.63,16
14+
45,1.58,16
15+
51,3.62,36
16+
52,1.45,15
17+
53,13.41,134
18+
54,2.5,25
19+
55,46.66,467
20+
56,9.02,90
21+
57,3.48,35
22+
58,2.51,25
23+
59,3.42,34
24+
65,3.06,31
25+
66,1.17,12
26+
67,15.88,159
27+
68,12.86,129
28+
69,40.64,406
29+
70,5,50
30+
71,99.99,999
31+
72,7.53,75
32+
77,4.19,42
33+
79,8.08,81
34+
80,26.17,262
35+
81,10.85,109
36+
82,5.73,57
37+
83,16.15,162
38+
84,4.3,43
39+
85,1.36,14
40+
86,1.62,16
41+
91,4.76,48
42+
92,11.35,114
43+
93,61.4,614
44+
94,8.79,88
45+
95,2.72,27
46+
96,7.47,75
47+
97,1.96,20
48+
105,3.07,31
49+
107,5.78,58
50+
108,1.34,13
51+
109,5.02,50
52+
111,2.96,30
53+
121,19.93,199
54+
122,1.59,16
55+
136,8.77,88
56+
137,1.04,10
57+
139,2.18,22

0 commit comments

Comments
 (0)