-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
plot.add-boxwhisker
- A second take on box-whisker plots (#287)
In PR #265, I implemented a box and whisker chart following the style of other charts, rather than implementing it as a plot. I believe this was a mistake as it quickly became cumbersome adding more features. Rather than implement a box and whisker chart all in one go, I've opted to make a pull request where drawing a single box and whisker is handled by a plot. ![image](https://github.com/johannes-wolf/cetz/assets/6299280/57af47e3-d6c9-4cee-8aa1-ae0b073c5b3f)
- Loading branch information
1 parent
94af05b
commit 27daf02
Showing
6 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
#import "../palette.typ" | ||
#import "../plot.typ" | ||
#import "../../draw.typ" | ||
#import "../../canvas.typ" | ||
|
||
#let boxwhisker-default-style = ( | ||
axes: (tick: (length: -0.1)), | ||
grid: none, | ||
) | ||
|
||
/// Add one or more box or whisker plots | ||
/// | ||
/// - 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 | ||
/// - 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)} | ||
|
||
let x-tic-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-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 | ||
) | ||
} | ||
} | ||
) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
#import "../../draw.typ" | ||
#import "../../util.typ" | ||
|
||
/// Add one or more box or whisker plots | ||
/// | ||
/// - 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 | ||
/// )``` | ||
/// - 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 | ||
} | ||
|
||
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 | ||
|
||
let max-value = calc.max( | ||
0,data.max, | ||
..data.at("outliers", default: (0,)) | ||
) | ||
|
||
let min-value = calc.min( | ||
0,data.min, | ||
..data.at("outliers", default: (0,)) | ||
) | ||
|
||
let max-value = util.max(data.max, ..data.at("outliers", default: ())) | ||
let min-value = util.min(data.min, ..data.at("outliers", default: ())) | ||
|
||
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) | ||
|
||
// 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) | ||
} | ||
|
||
(( | ||
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: (:) | ||
) }),) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
#set page(width: auto, height: auto) | ||
#import "/src/lib.typ": * | ||
|
||
#let data0 = ( | ||
( | ||
label: "Control", | ||
min: 10,q1: 25,q2: 50, | ||
q3: 75,max: 90 | ||
), | ||
( | ||
label: "Condition aB", | ||
min: 32,q1: 54,q2: 60, | ||
q3: 69,max: 73, | ||
outliers: (18, 23, 78,) | ||
), | ||
) | ||
|
||
#box( canvas({ | ||
chart.boxwhisker( | ||
y-min:0, | ||
y-max: 100, | ||
size: (10, 10), | ||
label-key: "label", | ||
data0 | ||
) | ||
})) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
#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 | ||
) | ||
|
||
#box(stroke: 2pt + red, canvas({ | ||
import draw: * | ||
|
||
plot.plot(size: (10, 10), | ||
y-min: 0, | ||
y-max: 100, | ||
{ | ||
plot.add-boxwhisker((x: 1, ..data)) | ||
}) | ||
})) | ||
|
||
#box(stroke: 2pt + red, canvas({ | ||
import draw: * | ||
|
||
plot.plot(size: (10, 10), | ||
y-min: 0, | ||
y-max: 100, | ||
{ | ||
plot.add-boxwhisker(( | ||
(x: 1, ..data), | ||
(x: 2, ..data), | ||
(x: 3, ..data), | ||
(x: 4, ..data), | ||
)) | ||
}) | ||
})) |