Skip to content

Commit

Permalink
plot: Add boxwhisker plot by @JamesxX
Browse files Browse the repository at this point in the history
  • Loading branch information
johannes-wolf committed Oct 26, 2023
1 parent df09fa5 commit a5b7168
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 104 deletions.
1 change: 1 addition & 0 deletions src/lib/plot.typ
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#import "axes.typ"
#import "palette.typ"
#import "../util.typ"
#import "../draw.typ"

#import "plot/sample.typ": sample-fn, sample-fn2
#import "plot/line.typ": add, add-hline, add-vline
Expand Down
176 changes: 87 additions & 89 deletions src/lib/plot/boxwhisker.typ
Original file line number Diff line number Diff line change
Expand Up @@ -6,109 +6,107 @@
/// - data (array, dictionary): dictionary or array of dictionaries containing the
/// needed entries to plot box and whisker plot.
///
/// *Examples:*
/// - ```( x: 1 // Location on x-axis
/// outliers: (7, 65, 69), // Optional
/// min: 15, max: 60 // Minimum and maximum
/// q1: 25, // Quartiles
/// q2: 35,
/// q3: 50
/// )```
/// The following fields are supported:
/// - `x` (number) X-axis value
/// - `min` (number) Minimum value
/// - `max` (number) Maximum value
/// - `q1`, `q2`, `q3` (number) Quartiles from low to high
/// - `outliers` (array of numbers) Optional outliers
///
/// *Examples:*
/// - ```(x: 1 // Location on x-axis
/// outliers: (7, 65, 69), // Optional
/// min: 15, max: 60 // Minimum and maximum
/// q1: 25, // Quartiles
/// q2: 35,
/// q3: 50
/// )```
/// - axes (array): Name of the axes to use ("x", "y"), note that not all
/// plot styles are able to display a custom axis!
/// - style (style): Style to use, can be used with a palette function
/// - box-width (float): Width from edge-to-edge of the box of the box and whisker in plot units. Defaults to 0.75
/// - whisker-width (float): Width from edge-to-edge of the whisker of the box and whisker in plot units. Defaults to 0.5
/// - mark (string): Mark to use for plotting outliers. Set `none` to disable. Defaults to "x"
/// - mark-size (float): Size of marks for plotting outliers. Defaults to 0.15
#let add-boxwhisker(
data,
axes: ("x", "y"),
style: (:),
box-width: 0.75,
whisker-width: 0.5,
mark: "*",
mark-size: 0.15
) = {

if type(data) == array {
for it in data{
add-boxwhisker(
it,
axes:axes,
style: style,
box-width: box-width,
whisker-width: whisker-width,
mark: mark,
mark-size: mark-size
)
}
return
#let add-boxwhisker(data,
axes: ("x", "y"),
style: (:),
box-width: 0.75,
whisker-width: 0.5,
mark: "*",
mark-size: 0.15) = {
// Add multiple boxes as multiple calls to
// add-boxwhisker
if type(data) == array {
for it in data {
add-boxwhisker(
it,
axes:axes,
style: style,
box-width: box-width,
whisker-width: whisker-width,
mark: mark,
mark-size: mark-size)
}
return
}

assert( "x" in data, message: "Specify the x value at which to display the box and whisker")
assert( "min" in data, message: "Specify the q1, the minimum excluding outliers")
assert( "q1" in data, message: "Specify the q1, the lower quartile")
assert( "q2" in data, message: "Specify the q2, the median")
assert( "q3" in data, message: "Specify the q3, the upper quartile")
assert( "max" in data, message: "Specify the q1, the minimum excluding outliers")

// Calculate y-domain
assert("x" in data, message: "Specify 'x', the x value at which to display the box and whisker")
assert("q1" in data, message: "Specify 'q1', the lower quartile")
assert("q2" in data, message: "Specify 'q2', the median")
assert("q3" in data, message: "Specify 'q3', the upper quartile")
assert("min" in data, message: "Specify 'min', the minimum excluding outliers")
assert("max" in data, message: "Specify 'max', the maximum excluding outliers")
assert(data.q1 <= data.q2 and data.q2 <= data.q3,
message: "The quartiles q1, q2 and q3 must follow q1 < q2 < q3")
assert(data.min <= data.q1 and data.max >= data.q2,
message: "The minimum and maximum must be <= q1 and >= q3")

let max-value = calc.max(
0,data.max,
..data.at("outliers", default: (0,))
)
// Y domain
let max-value = util.max(data.max, ..data.at("outliers", default: ()))
let min-value = util.min(data.min, ..data.at("outliers", default: ()))

let min-value = calc.min(
0,data.min,
..data.at("outliers", default: (0,))
)
let prepare(self, ctx) = {
return self
}

let max-value = util.max(data.max, ..data.at("outliers", default: ()))
let min-value = util.min(data.min, ..data.at("outliers", default: ()))
let stroke(self, ctx) = {
let data = self.bw-data

let prepare(self, ctx) = {
return self
}

let stroke(self, ctx) = {
let data = self.bw-data

// Box
draw.rect((data.x - box-width / 2, data.q1),
(data.x + box-width / 2, data.q3),
..self.style)
// Box
draw.rect((data.x - box-width / 2, data.q1),
(data.x + box-width / 2, data.q3),
..self.style)

// Mean
draw.line((data.x - box-width / 2, data.q2),
(data.x + box-width / 2, data.q2),
..self.style)
// Mean
draw.line((data.x - box-width / 2, data.q2),
(data.x + box-width / 2, data.q2),
..self.style)

// whiskers
let whisker(x, start, end) = {
draw.line((x, start),(x, end),..self.style)
draw.line((x - whisker-width / 2, end),(x + whisker-width / 2, end), ..self.style)
}
whisker(data.x, data.q3, data.max)
whisker(data.x, data.q1, data.min)
// whiskers
let whisker(x, start, end) = {
draw.line((x, start),(x, end),..self.style)
draw.line((x - whisker-width / 2, end),(x + whisker-width / 2, end), ..self.style)
}
whisker(data.x, data.q3, data.max)
whisker(data.x, data.q1, data.min)
}

((
type: "boxwhisker",
axes: axes,
bw-data: data,
style: style,
plot-prepare: prepare,
plot-stroke: stroke,
x-domain: (
data.x - calc.max(whisker-width, box-width),
data.x + calc.max(whisker-width, box-width)),
y-domain: (min-value, max-value),
) + (if "outliers" in data { (
data: data.outliers.map(it=>(data.x, it)),
mark: mark,
mark-size: mark-size,
mark-style: (:)
) }),)
}
((
type: "boxwhisker",
axes: axes,
bw-data: data,
style: style,
plot-prepare: prepare,
plot-stroke: stroke,
x-domain: (data.x - calc.max(whisker-width, box-width),
data.x + calc.max(whisker-width, box-width)),
y-domain: (min-value, max-value),
) + (if "outliers" in data { (
type: "boxwhisker-outliers",
data: data.outliers.map(it => (data.x, it)),
mark: mark,
mark-size: mark-size,
mark-style: (:)
) }),)
}
47 changes: 32 additions & 15 deletions tests/plot/boxwhisker/test.typ
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
#set page(width: auto, height: auto)
#import "/src/lib.typ": *

#let data = (
outliers: (7, 65, 69),
min: 15,
q1: 25,
q2: 35,
q3: 50,
max: 60
)
#let box1 = (
outliers: (7, 65, 69),
min: 15,
q1: 25,
q2: 35,
q3: 50,
max: 60)

#let box2 = (
min: -1,
q1: 0,
q2: 3,
q3: 6,
max: 8)

#box(stroke: 2pt + red, canvas({
import draw: *
Expand All @@ -17,22 +23,33 @@
y-min: 0,
y-max: 100,
{
plot.add-boxwhisker((x: 1, ..data))
plot.add-boxwhisker((x: 1, ..box1))
})
}))

#box(stroke: 2pt + red, canvas({
import draw: *

plot.plot(size: (10, 10),
y-min: 0,
y-max: 100,
y-min: 0, y-max: 100,
{
plot.add-boxwhisker((
(x: 1, ..data),
(x: 2, ..data),
(x: 3, ..data),
(x: 4, ..data),
(x: 1, ..box1),
(x: 2, ..box1),
(x: 3, ..box1),
(x: 4, ..box1),
))
})
}))

// Test auto-sizing of the plot
#box(stroke: 2pt + red, canvas({
import draw: *

plot.plot(size: (10, 10), {
plot.add-boxwhisker((
(x: 1, ..box1),
(x: 2, ..box2),
))
})
}))

0 comments on commit a5b7168

Please sign in to comment.