Skip to content

Commit

Permalink
Violin plot (#25)
Browse files Browse the repository at this point in the history
* begin work on violin plot

* testing

* Violin plots (draft)

* Better violin plots

* Better violin plots

* documentation and clean

* update test

* Add to manual and fix typo

* rename to `add-violin`

* document kernal signature

* update tests
  • Loading branch information
jamesrswift authored Jul 31, 2024
1 parent 2e7158a commit e151c4d
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 1 deletion.
2 changes: 1 addition & 1 deletion manual.typ
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ module imported into the namespace.
= Plot

#doc-style.parse-show-module("/src/plot.typ")
#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats") {
#for m in ("line", "bar", "boxwhisker", "contour", "errorbar", "annotation", "formats", "violin") {
doc-style.parse-show-module("/src/plot/" + m + ".typ")
}

Expand Down
1 change: 1 addition & 0 deletions src/plot.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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/violin.typ": add-violin
#import "/src/plot/formats.typ"
#import plot-legend: add-legend

Expand Down
135 changes: 135 additions & 0 deletions src/plot/violin.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#import "/src/cetz.typ": draw
#import "util.typ"
#import "sample.typ"

#let kernel-normal(x, stdev: 1.5) = {
(1/calc.sqrt(2*calc.pi*calc.pow(stdev,2))) * calc.exp( - (x*x)/(2*calc.pow(stdev,2)))
}

#let _violin-render(self, ctx, violin, filling: true) = {
let path = range(self.samples)
.map((t)=>violin.min + (violin.max - violin.min) * (t /self.samples ))
.map((u)=>(u, (violin.convolve)(u)))
.map(((u,v)) => {
(violin.x-position + v, u)
})

if self.side == "both"{
path += path.rev().map(((x,y))=> {(2 * violin.x-position - x,y)})
} else if self.side == "left"{
path = path.map( ((x,y))=>{(2 * violin.x-position - x,y)})
}

let (x, y) = (ctx.x, ctx.y)
let stroke-paths = util.compute-stroke-paths(path, (x.min, y.min), (x.max, y.max))

for p in stroke-paths{
let args = arguments(..p, closed: self.side == "both")
if filling {
args = arguments(..args, stroke: none)
} else {
args = arguments(..args, fill: none)
}
draw.line(..self.style, ..args)
}
}

#let _plot-prepare(self, ctx) = {
self.violins = self.data.map(entry=> {
let points = entry.at(self.y-key)
let (min, max) = (calc.min(..points), calc.max(..points))
let range = calc.abs(max - min)
(
x-position: entry.at(self.x-key),
points: points,
length: points.len(),
min: min - (self.extents * range),
max: max + (self.extents * range),
convolve: (t) => {
points.map((y)=>(self.kernel)((y - t)/self.bandwidth)).sum() / (points.len() * self.bandwidth)
}
)
})
return self
}

#let _plot-stroke(self, ctx) = {
for violin in self.violins {
_violin-render(self, ctx, violin, filling: false)
}
}

#let _plot-fill(self, ctx) = {
for violin in self.violins {
_violin-render(self, ctx, violin, filling: true)
}
}

#let _plot-legend-preview(self) = {
draw.rect((0,0), (1,1), ..self.style)
}


/// Add a violin plot
///
/// A violin plot is a chart that can be used to compare the distribution of continuous
/// data between categories.
///
/// - data (array): Array of data items. An item is an array containing an `x` and one
/// or more `y` values.
/// - x-key (int, string): Key to use for retreiving the `x` position of the violin.
/// - y-key (int, string): Key to use for retreiving values of points within the category.
/// - side (string): The sides of the violin to be rendered:
/// / left: Plot only the left side of the violin.
/// / right: Plot only the right side of the violin.
/// / both: Plot both sides of the violin.
/// - kernel (function): The kernel density estimator function, which takes a single
/// `x` value relative to the center of a distribution (0) and
/// normalized by the bandwidth
/// - bandwidth (float): The smoothing parameter of the kernel.
/// - extents (float): The extension of the domain, expressed as a fraction of spread.
/// - samples (int): The number of samples of the kernel to render.
/// - style (dictionary): Style override dictionary.
/// - mark-style (dictionary): (unused, will eventually be used to render interquartile ranges).
/// - axes (axes): (unstable, documentation to follow once completed).
/// - label (none, content): The name of the category to be shown in the legend.
#let add-violin(
data,
x-key: 0,
y-key: 1,
side: "right",
kernel: kernel-normal.with(stdev: 1.5),
bandwidth: 1,
extents: 0.25,

samples: 50,
style: (:),
mark-style: (:),
axes: ("x", "y"),
label: none,
) = {

((
type: "violins",

data: data,
x-key: x-key,
y-key: y-key,
side: side,
kernel: kernel,
bandwidth: bandwidth,
extents: extents,

samples: samples,
style: style,
mark-style: mark-style,
axes: axes,
label: label,

plot-prepare: _plot-prepare,
plot-stroke: _plot-stroke,
plot-fill: _plot-fill,
plot-legend-preview: _plot-legend-preview,
),)

}
Binary file added tests/plot/violin/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions tests/plot/violin/test.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#set page(width: auto, height: auto)
#import "/src/lib.typ": *
#import "/src/cetz.typ": *
#import "/tests/helper.typ": *

/* Empty plot */
#test-case({
import draw: *

draw.set-style(
axes: (
stroke: 0.55pt,
tick: (
stroke: 0.5pt,
)
),
legend: (
stroke: none,
)
)

let default-colors = (palette.blue-colors.at(3), palette.pink-colors.at(3))

plot.plot(size: (9, 6),

y-label: [Age],
y-min: -10, y-max: 20,
y-tick-step: 10, y-minor-tick-step: 5,
y-grid: "major",

x-label: [Class],
x-min: -0.5, x-max: 2.5,
x-tick-step: none,
x-ticks: ( (0, [First]), (1, [Second]), (2, [Third])),

plot-style: (i) => {
let color = default-colors.at(calc.rem(i, default-colors.len()))
(stroke: color + 0.75pt, fill: color.lighten(75%))
},
{
let vals = (
(0,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)),
(1,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)),
(2,(5,4,6,8,5.1,4.1,1,5.2,5.3,5.4,4.2,2,5.5,4.3,6,5,4,5,8,4,5,)),
)

cetz-plot.plot.add-violin(
vals,
extents: 0.5,
side: "left",
bandwidth: 0.45,
label: [Male],
)

cetz-plot.plot.add-violin(
vals,
extents: 0.5,
side: "right",
bandwidth: 0.5,
label: [Female]
)
})
})

0 comments on commit e151c4d

Please sign in to comment.