Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plot: Boxwhisker fixups #293

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 52 additions & 53 deletions src/lib/chart/boxwhisker.typ
Original file line number Diff line number Diff line change
Expand Up @@ -8,69 +8,68 @@
grid: none,
)

/// Add one or more box or whisker plots
/// Add one or more box or whisker plots.
///
/// - data (array, dictionary): dictionary or array of dictionaries containing the
/// - 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
/// )```
/// - size (array) : Size of chart. If the second entry is auto, it automatically scales to accomodate the number of entries plotted
/// See `plot.add-boxwhisker` for more details.
///
/// *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
/// )```
/// - size (array) : Size of chart. If the second entry is auto, it automatically scales to accommodate the number of entries plotted
/// - y-min (float) : Lower end of y-axis range. If auto, defaults to lowest outlier or lowest min.
/// - y-max (float) : Upper end of y-axis range. If auto, defaults to greatest outlier or greatest max.
/// - label-key (integer, string): Index in the array where labels of each entry is stored
/// - 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
/// - ..arguments (variadic): Additional arguments are passed to `plot.plot`
#let boxwhisker( data,
size: (1, auto),
y-min: auto,
y-max: auto,
label-key: 0,
box-width: 0.75,
whisker-width: 0.5,
mark: "*",
mark-size: 0.15,
..arguments
) = {
// import draw: *

if type(data) == dictionary { data = (data,) }

if size.at(1) == auto {size.at(1) = (data.len() + 1)}
/// - ..arguments (any): Additional arguments are passed to `plot.plot`
#let boxwhisker(data,
size: (1, auto),
y-min: auto,
y-max: auto,
label-key: 0,
box-width: 0.75,
whisker-width: 0.5,
mark: "*",
mark-size: 0.15,
..arguments
) = {
if type(data) == dictionary { data = (data,) }

let x-tic-list = data.enumerate().map(((i, t)) => {
(i + 1, t.at(label-key, default: i))
})
if size.at(1) == auto {size.at(1) = (data.len() + 1)}

plot.plot(
size: size,
x-tick-step: none,
x-ticks: x-tic-list,
y-min: y-min,
y-max: y-max,
x-label: none,
..arguments,
{
for (i, row) in data.enumerate() {
plot.add-boxwhisker(
( x: i + 1, ..row),
box-width: box-width,
whisker-width: whisker-width,
style: (:),
mark: mark,
mark-size: mark-size
)
}
}
)
let x-tick-list = data.enumerate().map(((i, t)) => {
(i + 1, t.at(label-key, default: i))
})

}
plot.plot(
size: size,
x-tick-step: none,
x-ticks: x-tick-list,
y-min: y-min,
y-max: y-max,
x-label: none,
..arguments,
{
for (i, row) in data.enumerate() {
plot.add-boxwhisker(
(x: i + 1, ..row),
box-width: box-width,
whisker-width: whisker-width,
style: (:),
mark: mark,
mark-size: mark-size
)
}
}
)
}
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: (:)
) }),)
}
Binary file added tests/chart/boxwhisker/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 4 additions & 5 deletions tests/chart/boxwhisker/test.typ
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
),
)

#box( canvas({
#box(canvas({
chart.boxwhisker(
y-min:0,
y-max: 100,
size: (10, 10),
y-min: 0,
y-max: 100,
label-key: "label",
data0
)
data0)
}))
Binary file added tests/plot/boxwhisker/ref.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading