diff --git a/CHANGES.md b/CHANGES.md index 37aca18d5..b6c1fe91c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,10 @@ # 0.3.0 CeTZ 0.3.0 requires Typst 0.11.0 -The licence changed from Apache-2.0 to GPLv3. +The licence changed from Apache-2.0 to LGPLv3. + +CeTZ' plotting and charting functionality has been moved to a separate +package called `cetz-plot`. ## Canvas - Fixed a bug with `#set place(float: true)` affecting the canvas. @@ -24,6 +27,7 @@ The licence changed from Apache-2.0 to GPLv3. allow for centered marks. ## Plot +- **BREAKING** The plot library has been moved out of cetz - Added support for automatically adding axis breaks (sawtooth lines) by setting the `break` attribute of an axis to `true`. - Added a new errorbar function: `add-errorbar` @@ -32,6 +36,7 @@ The licence changed from Apache-2.0 to GPLv3. - **BREAKING** Legend anchors got renamed and do not use the legend prefix anymore ## Chart +- **BREAKING** The chart library has been moved out of cetz - Added errorbar support for bar- and columncharts - Piecharts now support a legend (see `legend.label` style) - **BREAKING** Legend anchors got renamed and do not use the legend prefix anymore diff --git a/README.md b/README.md index 9c1a6cded..a57cdab25 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,10 @@ To use this package, simply add the following code to your document: }) ``` +## CeTZ Libraries + +- [cetz-plot - Plotting and Charts Library](https://github.com/cetz-package/cetz-plot) + ## Installing To install the CeTZ package under [your local typst package dir](https://github.com/typst/packages?tab=readme-ov-file#local-packages) you can use the `install` script from the repository. diff --git a/gallery/barchart.typ b/gallery/barchart.typ index ed5027655..bcc517922 100644 --- a/gallery/barchart.typ +++ b/gallery/barchart.typ @@ -1,4 +1,5 @@ #import "@preview/cetz:0.2.2": canvas, chart, draw +#import "@preview/cetz-plot:0.1.0": chart #set page(width: auto, height: auto, margin: .5cm) diff --git a/gallery/pie-chart.typ b/gallery/pie-chart.typ index fbadbe4ff..ef2e67880 100644 --- a/gallery/pie-chart.typ +++ b/gallery/pie-chart.typ @@ -1,4 +1,6 @@ #import "@preview/cetz:0.2.2" +#import "@preview/cetz-plot:0.1.0": chart + #set page(width: auto, height: auto, margin: .5cm) #let data = ( diff --git a/gallery/plot.typ b/gallery/plot.typ index 9981dd57d..702e127cd 100644 --- a/gallery/plot.typ +++ b/gallery/plot.typ @@ -1,4 +1,5 @@ -#import "@preview/cetz:0.2.2": canvas, plot +#import "@preview/cetz:0.2.2": canvas +#import "@preview/cetz-plot:0.1.0": plot #set page(width: auto, height: auto, margin: .5cm) diff --git a/manual.typ b/manual.typ index 05c2067dd..7a1c7e60f 100644 --- a/manual.typ +++ b/manual.typ @@ -594,248 +594,6 @@ The tree library allows the drawing diagrams with simple tree layout algorithms #doc-style.parse-show-module("/src/lib/tree.typ") -== Plot - -The library `plot` of CeTZ allows plotting data. - -=== Types - -Types commonly used by function of the `plot` library: -- #doc-style.show-type("domain"): Tuple representing a functions domain as closed interval. - Example domains are: `(0, 1)` for $[0, 1]$ or - `(-calc.pi, calc.pi)` for $[-pi, pi]$. -- #doc-style.show-type("axes"): Tuple of axis names. Plotting functions taking an `axes` tuple - will use those axes as their `x` and `y` axis for plotting. - To rotate a plot, you can simply swap its axes, for example `("y", "x")`. -- #doc-style.show-type("mark"): Plots feature their own set of marks. The following mark symbols are - available: - ```example-vertical - let marks = ("+", "x", "-", "|", "o", "square", "triangle") - cetz.plot.plot(size: (14, 1), x-min: 0, x-max: marks.len() + 1, - x-ticks: marks.enumerate().map(((i, s)) => (i+1, raw(s))), - x-tick-step: none, y-tick-step: none, - x-label: none, y-label: none, - { - for (i, s) in marks.enumerate() { - cetz.plot.add(((i + 1, 0),), mark: s, mark-style: (stroke: blue, fill: white), mark-size: .5) - } - }) - ``` - -#doc-style.parse-show-module("/src/lib/plot.typ") - -=== Legends -A legend for a plot will be drawn if at least one set of data with a label that is not `none` is given. -The following anchors are available when placing a legend on a plot: - - `north` - - `south` - - `east` - - `west` - - `north-east` - - `north-west` - - `south-east` - - `south-west` - - `inner-north` - - `inner-south` - - `inner-east` - - `inner-west` - - `inner-north-east` - - `inner-north-west` - - `inner-south-east` - - `inner-south-west` -```example -import cetz.plot -plot.plot( - size: (3,2), - x-tick-step: none, - y-tick-step: none, - legend: "north", { - plot.add( - ((-1, -1), (1, 1)), - mark: "o", - label: $ f(x) $ - ) -}) -``` - -==== Styling -*Root:* `legend` -===== Keys -#doc-style.show-parameter-block("orientation", ("direction"), default: ttb, [ - The direction the legend items get laid out to. -]) -#doc-style.show-parameter-block("default-position", ("string", "coordinate"), default: "legend.north-east", [ - The default position the legend gets placed at. -]) -#doc-style.show-parameter-block("layer", ("number"), default: 1, [ - The layer index the legend gets drawn at, see on-layer. -]) -#doc-style.show-parameter-block("fill", ("paint"), default: rgb(255,255,255,200), [ - The legends frame background color. -]) -#doc-style.show-parameter-block("stroke", ("stroke"), default: black, [ - The legends frame stroke style. -]) -#doc-style.show-parameter-block("padding", ("float"), default: .1, [ - The legends frame padding, that is the distance added between its items and its frame. -]) -#doc-style.show-parameter-block("offset", ("tuple"), default: (0,0), [ - An offset tuple (x and y coordinates) to add to the legends position. -]) -#doc-style.show-parameter-block("spacing", ("number"), default: .1, [ - The spacing between the legend position and its frame. -]) -#doc-style.show-parameter-block("item.spacing", ("number"), default: .05, [ - The spacing between two legend items in canvas units. -]) -#doc-style.show-parameter-block("item.preview.width", ("number"), default: .75, [ - The width of a legend items preview picture, a small preview of the graph the legend item belongs to. -]) -#doc-style.show-parameter-block("item.preview.height", ("number"), default: .3, [ - The height of a legend items preview picture. -]) -#doc-style.show-parameter-block("item.preview.margin", ("number"), default: .1, [ - Margin between the preview picture and the item label. -]) - - -#doc-style.parse-show-module("/src/lib/plot/line.typ") -#doc-style.parse-show-module("/src/lib/plot/contour.typ") -#doc-style.parse-show-module("/src/lib/plot/boxwhisker.typ") -#doc-style.parse-show-module("/src/lib/plot/bar.typ") -#doc-style.parse-show-module("/src/lib/plot/annotation.typ") -#doc-style.parse-show-module("/src/lib/plot/sample.typ") - -=== Examples - -```example -import cetz.plot -plot.plot(size: (3,2), x-tick-step: calc.pi, y-tick-step: 1, - x-format: v => $#{v/calc.pi} pi$, { - plot.add(domain: (0, 4*calc.pi), calc.sin, - samples: 15, line: "hvh", style: (mark: (stroke: blue))) - plot.add(domain: (0, 4*calc.pi), calc.sin) -}) -``` - -```example -import cetz.plot -import cetz.palette - -// Let ticks point outwards by giving them negative length -set-style(axes: (tick: (length: -.2, minor-length: -.1))) - -// Plot something -plot.plot(size: (3,3), x-tick-step: 1, x-minor-tick-step: .2, - y-tick-step: 1, y-minor-tick-step: .2, { - let z(x, y) = { - (1 - x/2 + calc.pow(x,5) + calc.pow(y,3)) * calc.exp(-(x*x) - (y*y)) - } - plot.add-contour(x-domain: (-2, 3), y-domain: (-3, 3), - z, z: (.1, .4, .7), fill: true) -}) -``` - -=== Styling - -The following style keys can be used (in addition to the standard keys) -to style plot axes. Individual axes can be styled differently by -using their axis name as key below the `axes` root. - -```typc -set-style(axes: ( /* Style for all axes */ )) -set-style(axes: (bottom: ( /* Style axis "bottom" */))) -``` - -Axis names to be used for styling: -- School-Book and Left style: - - `x`: X-Axis - - `y`: Y-Axis -- Scientific style: - - `left`: Y-Axis - - `right`: Y2-Axis - - `bottom`: X-Axis - - `top`: X2-Axis - -#doc-style.parse-show-module("/src/lib/axes.typ") - -==== Default `scientific` Style -#raw(repr(axes.default-style-scientific)) - -==== Default `school-book` Style -#raw(repr(axes.default-style-schoolbook)) - -== Chart - -With the `chart` library it is easy to draw charts. - -#doc-style.parse-show-module("/src/lib/chart/barchart.typ") -#doc-style.parse-show-module("/src/lib/chart/columnchart.typ") -#doc-style.parse-show-module("/src/lib/chart/piechart.typ") -#doc-style.parse-show-module("/src/lib/chart/boxwhisker.typ") - -=== Examples -- Bar Chart -```example-vertical -import cetz.chart -// Left - Basic -let data = (("A", 10), ("B", 20), ("C", 13)) -group(name: "a", { - chart.barchart(size: (4, 3), data) -}) -// Center - Clustered -let data = (("A", 10, 12, 22), ("B", 20, 1, 7), ("C", 13, 8, 9)) -group(name: "b", anchor: "south-west", { - anchor("default", "a.south-east") - chart.barchart(size: (4, 3), mode: "clustered", value-key: (1,2,3), data) -}) -// Right - Stacked -let data = (("A", 10, 12, 22), ("B", 20, 1, 7), ("C", 13, 8, 9)) -group(name: "c", anchor: "south-west", { - anchor("default", "b.south-east") - chart.barchart(size: (4, 3), mode: "stacked", value-key: (1,2,3), data) -}) -``` - -=== Examples -- Column Chart - -==== Basic, Clustered and Stacked -```example-vertical -import cetz.chart -// Left - Basic -let data = (("A", 10), ("B", 20), ("C", 13)) -group(name: "a", { - chart.columnchart(size: (4, 3), data) -}) -// Center - Clustered -let data = (("A", 10, 12, 22), ("B", 20, 1, 7), ("C", 13, 8, 9)) -group(name: "b", anchor: "south-west", { - anchor("default", "a.south-east") - chart.columnchart(size: (4, 3), mode: "clustered", value-key: (1,2,3), data) -}) -// Right - Stacked -let data = (("A", 10, 12, 22), ("B", 20, 1, 7), ("C", 13, 8, 9)) -group(name: "c", anchor: "south-west", { - anchor("default", "b.south-east") - chart.columnchart(size: (4, 3), mode: "stacked", value-key: (1,2,3), data) -}) -``` - -=== Styling - -Charts share their axis system with plots and therefore can be -styled the same way, see @plot.style. - -==== Default `barchart` Style -#raw(repr(chart.barchart-default-style)) - -==== Default `columnchart` Style -#raw(repr(chart.columnchart-default-style)) - -==== Default `boxwhisker` Style -#raw(repr(chart.boxwhisker-default-style)) - -==== Default `piechart` Style -#raw(repr(chart.piechart-default-style)) == Palette diff --git a/src/lib.typ b/src/lib.typ index b90d3b354..d12ea12a2 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -17,9 +17,6 @@ #import "mark-shapes.typ" // Libraries -#import "lib/axes.typ" -#import "lib/plot.typ" -#import "lib/chart.typ" #import "lib/palette.typ" #import "lib/angle.typ" #import "lib/tree.typ" diff --git a/src/lib/axes.typ b/src/lib/axes.typ deleted file mode 100644 index e5ac46da6..000000000 --- a/src/lib/axes.typ +++ /dev/null @@ -1,832 +0,0 @@ -// CeTZ Library for drawing graph axes -#import "/src/util.typ" -#import "/src/draw.typ" -#import "/src/vector.typ" -#import "/src/styles.typ" -#import "/src/process.typ" -#import "/src/drawable.typ" -#import "/src/path-util.typ" - -#let typst-content = content - -/// Default axis style -/// -/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.]) -/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.]) -/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.]) -/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.]) -/// #show-parameter-block("stroke", "stroke", [Axis stroke style.]) -/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.]) -/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.]) -/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.]) -/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see @@on-layer() )]) -/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see @@on-layer() )]) -/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see @@on-layer() )]) -/// #show-parameter-block("padding", "number", [Extra distance between axes and plotting area. For schoolbook axes, this is the length of how much axes grow out of the plotting area.]) -/// #show-parameter-block("overshoot", "number", [School-book style axes only: Extra length to add to the end (right, top) of axes.]) -/// #show-parameter-block("tick.stroke", "stroke", [Major tick stroke style.]) -/// #show-parameter-block("tick.minor-stroke", "stroke", [Minor tick stroke style.]) -/// #show-parameter-block("tick.offset", ("number", "ratio"), [Major tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.minor-offset", ("number", "ratio"), [Minor tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.length", ("number"), [Major tick length.]) -/// #show-parameter-block("tick.minor-length", ("number", "ratio"), [Minor tick length, can be relative to the major tick length.]) -/// #show-parameter-block("tick.label.offset", ("number"), [Major tick label offset away from the tick.]) -/// #show-parameter-block("tick.label.angle", ("angle"), [Major tick label angle.]) -/// #show-parameter-block("tick.label.anchor", ("anchor"), [Anchor of major tick labels used for positioning.]) -/// #show-parameter-block("tick.label.show", ("auto", "bool"), default: auto, [Set visibility of tick labels. A value of `auto` shows tick labels for all but mirrored axes.]) -/// #show-parameter-block("grid.stroke", "stroke", [Major grid line stroke style.]) -/// #show-parameter-block("break-point.width", "number", [Axis break width along the axis.]) -/// #show-parameter-block("break-point.length", "number", [Axis break length.]) -/// #show-parameter-block("minor-grid.stroke", "stroke", [Minor grid line stroke style.]) -/// #show-parameter-block("shared-zero", ("bool", "content"), default: "$0$", [School-book style axes only: Content to display at the plots origin (0,0). If set to `false`, nothing is shown. Having this set, suppresses auto-generated ticks for $0$!]) -#let default-style = ( - tick-limit: 100, - minor-tick-limit: 1000, - auto-tick-factors: (1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10), // Tick factor to try - auto-tick-count: 11, // Number of ticks the plot tries to place - fill: none, - stroke: auto, - label: ( - offset: .2cm, // Axis label offset - anchor: auto, // Axis label anchor - angle: auto, // Axis label angle - ), - axis-layer: 0, - grid-layer: 0, - background-layer: 0, - padding: 0, - tick: ( - fill: none, - stroke: black + 1pt, - minor-stroke: black + .5pt, - offset: 0, - minor-offset: 0, - length: .1cm, // Tick length: Number - minor-length: 70%, // Minor tick length: Number, Ratio - label: ( - offset: .15cm, // Tick label offset - angle: 0deg, // Tick label angle - anchor: auto, // Tick label anchor - "show": auto, // Show tick labels for axes in use - ) - ), - break-point: ( - width: .75cm, - length: .15cm, - ), - grid: ( - stroke: (paint: gray.lighten(50%), thickness: 1pt), - ), - minor-grid: ( - stroke: (paint: gray.lighten(50%), thickness: .5pt), - ), -) - -// Default Scientific Style -#let default-style-scientific = util.merge-dictionary(default-style, ( - left: (tick: (label: (anchor: "east"))), - bottom: (tick: (label: (anchor: "north"))), - right: (tick: (label: (anchor: "west"))), - top: (tick: (label: (anchor: "south"))), - stroke: (cap: "square"), - padding: 0, -)) - -#let default-style-schoolbook = util.merge-dictionary(default-style, ( - x: (stroke: auto, fill: none, mark: (start: none, end: "straight"), - tick: (label: (anchor: "north"))), - y: (stroke: auto, fill: none, mark: (start: none, end: "straight"), - tick: (label: (anchor: "east"))), - label: (offset: .1cm), - origin: (label: (offset: .05cm)), - padding: .1cm, // Axis padding on both sides outsides the plotting area - overshoot: .5cm, // Axis end "overshoot" out of the plotting area - tick: ( - offset: -50%, - minor-offset: -50%, - length: .2cm, - minor-length: 70%, - ), - shared-zero: $0$, // Show zero tick label at (0, 0) -)) - -#let _prepare-style(ctx, style) = { - if type(style) != dictionary { return style } - - let res = util.resolve-number.with(ctx) - let rel-to(v, to) = { - if type(v) == ratio { - return v * to / 100% - } else { - return res(v) - } - } - - style.tick.length = res(style.tick.length) - style.tick.offset = rel-to(style.tick.offset, style.tick.length) - style.tick.minor-length = rel-to(style.tick.minor-length, style.tick.length) - style.tick.minor-offset = rel-to(style.tick.minor-offset, style.tick.minor-length) - style.tick.label.offset = res(style.tick.label.offset) - - // Break points - style.break-point.width = res(style.break-point.width) - style.break-point.length = res(style.break-point.length) - - // Padding - style.padding = res(style.padding) - - if "overshoot" in style { - style.overshoot = res(style.overshoot) - } - - return style -} - -#let _get-axis-style(ctx, style, name) = { - if not name in style { - return style - } - - style = styles.resolve(style, merge: style.at(name)) - return _prepare-style(ctx, style) -} - -#let _get-grid-type(axis) = { - let grid = axis.ticks.at("grid", default: false) - if grid == "major" or grid == true { return 1 } - if grid == "minor" { return 2 } - if grid == "both" { return 3 } - return 0 -} - -#let _inset-axis-points(ctx, style, axis, start, end) = { - if axis == none { return (start, end) } - - let (low, high) = axis.inset.map(v => util.resolve-number(ctx, v)) - - let is-horizontal = start.at(1) == end.at(1) - if is-horizontal { - start = vector.add(start, (low, 0)) - end = vector.sub(end, (high, 0)) - } else { - start = vector.add(start, (0, low)) - end = vector.sub(end, (0, high)) - } - return (start, end) -} - -#let _draw-axis-line(start, end, axis, is-horizontal, style) = { - let enabled = if axis != none and axis.show-break { - axis.min > 0 or axis.max < 0 - } else { false } - - if enabled { - let size = if is-horizontal { - (style.break-point.width, 0) - } else { - (0, style.break-point.width, 0) - } - - let up = if is-horizontal { - (0, style.break-point.length) - } else { - (style.break-point.length, 0) - } - - let add-break(is-end) = { - let a = () - let b = (rel: vector.scale(size, .3), update: false) - let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false) - let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false) - let e = (rel: vector.scale(size, .7), update: false) - let f = (rel: size) - - let mark = if is-end { - style.at("mark", default: none) - } - draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark) - } - - draw.merge-path({ - draw.move-to(start) - if axis.min > 0 { - add-break(false) - draw.line((rel: size, to: start), end, mark: style.at("mark", default: none)) - } else if axis.max < 0 { - draw.line(start, (rel: vector.scale(size, -1), to: end)) - add-break(true) - } - }, stroke: style.stroke) - } else { - draw.line(start, end, stroke: style.stroke, mark: style.at("mark", default: none)) - } -} - -// Construct Axis Object -// -// - min (number): Minimum value -// - max (number): Maximum value -// - ticks (dictionary): Tick settings: -// - step (number): Major tic step -// - minor-step (number): Minor tic step -// - unit (content): Tick label suffix -// - decimals (int): Tick float decimal length -// - label (content): Axis label -#let axis(min: -1, max: 1, label: none, - ticks: (step: auto, minor-step: none, - unit: none, decimals: 2, grid: false, - format: "float")) = ( - min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, -) - -// Format a tick value -#let format-tick-value(value, tic-options) = { - // Without it we get negative zero in conversion - // to content! Typst has negative zero floats. - if value == 0 { value = 0 } - - let round(value, digits) = { - calc.round(value, digits: digits) - } - - let format-float(value, digits) = { - $#round(value, digits)$ - } - - let format-sci(value, digits) = { - let exponent = if value != 0 { - calc.floor(calc.log(calc.abs(value), base: 10)) - } else { - 0 - } - - let ee = calc.pow(10, calc.abs(exponent + 1)) - if exponent > 0 { - value = value / ee * 10 - } else if exponent < 0 { - value = value * ee * 10 - } - - value = round(value, digits) - if exponent <= -1 or exponent >= 1 { - return $#value times 10^#exponent$ - } - return $#value$ - } - - if type(value) != typst-content { - let format = tic-options.at("format", default: "float") - if format == none { - value = [] - } else if type(format) == typst-content { - value = format - } else if type(format) == function { - value = (format)(value) - } else if format == "sci" { - value = format-sci(value, tic-options.at("decimals", default: 2)) - } else { - value = format-float(value, tic-options.at("decimals", default: 2)) - } - } else if type(value) != typst-content { - value = str(value) - } - - if tic-options.at("unit", default: none) != none { - value += tic-options.unit - } - return value -} - -// Get value on axis [0, 1] -// -// - axis (axis): Axis -// - v (number): Value -// -> float -#let value-on-axis(axis, v) = { - if v == none { return } - let (min, max) = (axis.min, axis.max) - let dt = max - min; if dt == 0 { dt = 1 } - - return (v - min) / dt -} - -// Compute list of linear ticks for axis -// -// - axis (axis): Axis -#let compute-linear-ticks(axis, style, add-zero: true) = { - let (min, max) = (axis.min, axis.max) - let dt = max - min; if (dt == 0) { dt = 1 } - let ticks = axis.ticks - let ferr = util.float-epsilon - let tick-limit = style.tick-limit - let minor-tick-limit = style.minor-tick-limit - - let l = () - if ticks != none { - let major-tick-values = () - if "step" in ticks and ticks.step != none { - assert(ticks.step >= 0, - message: "Axis tick step must be positive and non 0.") - if axis.min > axis.max { ticks.step *= -1 } - - let s = 1 / ticks.step - - let num-ticks = int(max * s + 1.5) - int(min * s) - assert(num-ticks <= tick-limit, - message: "Number of major ticks exceeds limit " + str(tick-limit)) - - let n = range(int(min * s), int(max * s + 1.5)) - for t in n { - let v = (t / s - min) / dt - if t / s == 0 and not add-zero { continue } - - if v >= 0 - ferr and v <= 1 + ferr { - l.push((v, format-tick-value(t / s, ticks), true)) - major-tick-values.push(v) - } - } - } - - if "minor-step" in ticks and ticks.minor-step != none { - assert(ticks.minor-step >= 0, - message: "Axis minor tick step must be positive") - - let s = 1 / ticks.minor-step - - let num-ticks = int(max * s + 1.5) - int(min * s) - assert(num-ticks <= minor-tick-limit, - message: "Number of minor ticks exceeds limit " + str(minor-tick-limit)) - - let n = range(int(min * s), int(max * s + 1.5)) - for t in n { - let v = (t / s - min) / dt - if v in major-tick-values { - // Prefer major ticks over minor ticks - continue - } - - if v != none and v >= 0 and v <= 1 + ferr { - l.push((v, none, false)) - } - } - } - - } - - return l -} - -// Get list of fixed axis ticks -// -// - axis (axis): Axis object -#let fixed-ticks(axis) = { - let l = () - if "list" in axis.ticks { - for t in axis.ticks.list { - let (v, label) = (none, none) - if type(t) in (float, int) { - v = t - label = format-tick-value(t, axis.ticks) - } else { - (v, label) = t - } - - v = value-on-axis(axis, v) - if v != none and v >= 0 and v <= 1 { - l.push((v, label, true)) - } - } - } - return l -} - -// Compute list of axis ticks -// -// A tick triple has the format: -// (rel-value: float, label: content, major: bool) -// -// - axis (axis): Axis object -#let compute-ticks(axis, style, add-zero: true) = { - let find-max-n-ticks(axis, n: 11) = { - let dt = calc.abs(axis.max - axis.min) - let scale = calc.floor(calc.log(dt, base: 10) - 1) - if scale > 5 or scale < -5 {return none} - - let (step, best) = (none, 0) - for s in style.auto-tick-factors { - s = s * calc.pow(10, scale) - - let divs = calc.abs(dt / s) - if divs >= best and divs <= n { - step = s - best = divs - } - } - return step - } - - if axis == none or axis.ticks == none { return () } - if axis.ticks.step == auto { - axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count) - } - if axis.ticks.minor-step == auto { - axis.ticks.minor-step = if axis.ticks.step != none { - axis.ticks.step / 5 - } else { - none - } - } - - let ticks = compute-linear-ticks(axis, style, add-zero: add-zero) - ticks += fixed-ticks(axis) - return ticks -} - -// Prepares the axis post creation. The given axis -// must be completely set-up, including its intervall. -// Returns the prepared axis -#let prepare-axis(ctx, axis, name) = { - let style = styles.resolve(ctx.style, root: "axes", - base: default-style-scientific) - style = _prepare-style(ctx, style) - style = _get-axis-style(ctx, style, name) - - if type(axis.inset) != array { - axis.inset = (axis.inset, axis.inset) - } - - axis.inset = axis.inset.map(v => util.resolve-number(ctx, v)) - - if axis.show-break { - if axis.min > 0 { - axis.inset.at(0) += style.break-point.width - } else if axis.max < 0 { - axis.inset.at(1) += style.break-point.width - } - } - - return axis -} - -// Draw inside viewport coordinates of two axes -// -// - size (vector): Axis canvas size (relative to origin) -// - origin (coordinates): Axis Canvas origin -// - x (axis): Horizontal axis -// - y (axis): Vertical axis -// - name (string,none): Group name -#let axis-viewport(size, x, y, origin: (0, 0), name: none, body) = { - draw.group(name: name, ctx => { - let origin = origin - let size = size - - origin.at(0) += x.inset.at(0) - size.at(0) -= x.inset.sum() - origin.at(1) += y.inset.at(0) - size.at(1) -= y.inset.sum() - - size = (rel: size, to: origin) - draw.set-viewport(origin, size, - bounds: (x.max - x.min, - y.max - y.min, - 0)) - draw.translate((-x.min, -y.min)) - body - }) -} - -// Draw grid lines for the ticks of an axis -// -// - cxt (context): -// - axis (dictionary): The axis -// - ticks (array): The computed ticks -// - low (vector): Start position of a grid-line at tick 0 -// - high (vector): End position of a grid-line at tick 0 -// - dir (vector): Normalized grid direction vector along the grid axis -// - style (style): Axis style -#let draw-grid-lines(ctx, axis, ticks, low, high, dir, style) = { - let offset = (0,0) - if axis.inset != none { - let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v)) - offset = vector.scale(vector.norm(dir), inset-low) - dir = vector.sub(dir, vector.scale(vector.norm(dir), inset-low + inset-high)) - } - - let kind = _get-grid-type(axis) - if kind > 0 { - for (distance, label, is-major) in ticks { - let offset = vector.add(vector.scale(dir, distance), offset) - let start = vector.add(low, offset) - let end = vector.add(high, offset) - - // Draw a major line - if is-major and (kind == 1 or kind == 3) { - draw.line(start, end, stroke: style.grid.stroke) - } - // Draw a minor line - if not is-major and kind >= 2 { - draw.line(start, end, stroke: style.minor-grid.stroke) - } - } - } -} - -// Place a list of tick marks and labels along a path -#let place-ticks-on-line(ticks, start, stop, style, flip: false, is-mirror: false) = { - let dir = vector.sub(stop, start) - let norm = vector.norm((-dir.at(1), dir.at(0), dir.at(2, default: 0))) - - let def(v, d) = { - return if v == none or v == auto {d} else {v} - } - - let show-label = style.tick.label.show - if show-label == auto { - show-label = not is-mirror - } - - for (distance, label, is-major) in ticks { - let offset = style.tick.offset - let length = if is-major { style.tick.length } else { style.tick.minor-length } - if flip { - offset *= -1 - length *= -1 - } - - let pt = vector.lerp(start, stop, distance) - let a = vector.add(pt, vector.scale(norm, offset)) - let b = vector.add(a, vector.scale(norm, length)) - - draw.line(a, b, stroke: style.tick.stroke) - - if show-label and label != none { - let offset = style.tick.label.offset - if flip { - offset *= -1 - length *= -1 - } - - let c = vector.sub(if length <= 0 { b } else { a }, - vector.scale(norm, offset)) - - let angle = def(style.tick.label.angle, 0deg) - let anchor = def(style.tick.label.anchor, "center") - - draw.content(c, [#label], angle: angle, anchor: anchor) - } - } -} - -// Draw up to four axes in an "scientific" style at origin (0, 0) -// -// - size (array): Size (width, height) -// - left (axis): Left (y) axis -// - bottom (axis): Bottom (x) axis -// - right (axis): Right axis -// - top (axis): Top axis -// - name (string): Object name -// - draw-unset (bool): Draw axes that are set to `none` -// - ..style (any): Style -#let scientific(size: (1, 1), - left: none, - right: auto, - bottom: none, - top: auto, - draw-unset: true, - name: none, - ..style) = { - import draw: * - - if right == auto { - if left != none { - right = left; right.is-mirror = true - } else { - right = none - } - } - if top == auto { - if bottom != none { - top = bottom; top.is-mirror = true - } else { - top = none - } - } - - group(name: name, ctx => { - let (w, h) = size - anchor("origin", (0, 0)) - - let style = style.named() - style = styles.resolve(ctx.style, merge: style, root: "axes", - base: default-style-scientific) - style = _prepare-style(ctx, style) - - // Compute ticks - let x-ticks = compute-ticks(bottom, style) - let y-ticks = compute-ticks(left, style) - let x2-ticks = compute-ticks(top, style) - let y2-ticks = compute-ticks(right, style) - - // Draw frame - if style.fill != none { - on-layer(style.background-layer, { - rect((0,0), (w,h), fill: style.fill, stroke: none) - }) - } - - // Draw grid - group(name: "grid", ctx => { - let axes = ( - ("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom), - ("top", (0,h), (0,0), (+w,0), x2-ticks, top), - ("left", (0,0), (w,0), (0,+h), y-ticks, left), - ("right", (w,0), (0,0), (0,+h), y2-ticks, right), - ) - for (name, start, end, direction, ticks, axis) in axes { - if axis == none { continue } - - let style = _get-axis-style(ctx, style, name) - let is-mirror = axis.at("is-mirror", default: false) - - if not is-mirror { - on-layer(style.grid-layer, { - draw-grid-lines(ctx, axis, ticks, start, end, direction, style) - }) - } - } - }) - - // Draw axes - group(name: "axes", { - let axes = ( - ("bottom", (0, 0), (w, 0), (0, -1), false, x-ticks, bottom,), - ("top", (0, h), (w, h), (0, +1), true, x2-ticks, top,), - ("left", (0, 0), (0, h), (-1, 0), true, y-ticks, left,), - ("right", (w, 0), (w, h), (+1, 0), false, y2-ticks, right,) - ) - let label-placement = ( - bottom: ("south", "north", 0deg), - top: ("north", "south", 0deg), - left: ("west", "south", 90deg), - right: ("east", "north", 90deg), - ) - - for (name, start, end, outsides, flip, ticks, axis) in axes { - let style = _get-axis-style(ctx, style, name) - let is-mirror = axis == none or axis.at("is-mirror", default: false) - let is-horizontal = name in ("bottom", "top") - - if style.padding != 0 { - let padding = vector.scale(outsides, style.padding) - start = vector.add(start, padding) - end = vector.add(end, padding) - } - - let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - - let path = _draw-axis-line(start, end, axis, is-horizontal, style) - on-layer(style.axis-layer, { - group(name: "axis", { - if draw-unset or axis != none { - path; - place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror) - } - }) - - if axis != none and axis.label != none and not is-mirror { - let offset = vector.scale(outsides, style.label.offset) - let (group-anchor, content-anchor, angle) = label-placement.at(name) - - if style.label.anchor != auto { - content-anchor = style.label.anchor - } - if style.label.angle != auto { - angle = style.label.angle - } - - content((rel: offset, to: "axis." + group-anchor), - [#axis.label], - angle: angle, - anchor: content-anchor) - } - }) - } - }) - }) -} - -// Draw two axes in a "school book" style -// -// - x-axis (axis): X axis -// - y-axis (axis): Y axis -// - size (array): Size (width, height) -// - x-position (number): X Axis position -// - y-position (number): Y Axis position -// - name (string): Object name -// - ..style (any): Style -#let school-book(x-axis, y-axis, - size: (1, 1), - x-position: 0, - y-position: 0, - name: none, - ..style) = { - import draw: * - - group(name: name, ctx => { - let (w, h) = size - anchor("origin", (0, 0)) - - let style = style.named() - style = styles.resolve( - ctx.style, - merge: style, - root: "axes", - base: default-style-schoolbook) - style = _prepare-style(ctx, style) - - let x-position = calc.min(calc.max(y-axis.min, x-position), y-axis.max) - let y-position = calc.min(calc.max(x-axis.min, y-position), x-axis.max) - let x-y = value-on-axis(y-axis, x-position) * h - let y-x = value-on-axis(x-axis, y-position) * w - - let shared-zero = style.shared-zero != false and x-position == 0 and y-position == 0 - - let x-ticks = compute-ticks(x-axis, style, add-zero: not shared-zero) - let y-ticks = compute-ticks(y-axis, style, add-zero: not shared-zero) - - // Draw grid - group(name: "grid", ctx => { - let axes = ( - ("x", (0,0), (0,h), (+w,0), x-ticks, x-axis), - ("y", (0,0), (w,0), (0,+h), y-ticks, y-axis), - ) - - for (name, start, end, direction, ticks, axis) in axes { - if axis == none { continue } - - let style = _get-axis-style(ctx, style, name) - on-layer(style.grid-layer, { - draw-grid-lines(ctx, axis, ticks, start, end, direction, style) - }) - } - }) - - // Draw axes - group(name: "axes", { - let axes = ( - ("x", (0, x-y), (w, x-y), (1, 0), false, x-ticks, x-axis), - ("y", (y-x, 0), (y-x, h), (0, 1), true, y-ticks, y-axis), - ) - let label-pos = ( - x: ("north", (0,-1)), - y: ("east", (-1,0)), - ) - - on-layer(style.axis-layer, { - for (name, start, end, dir, flip, ticks, axis) in axes { - let style = _get-axis-style(ctx, style, name) - - let pad = style.padding - let overshoot = style.overshoot - let vstart = vector.sub(start, vector.scale(dir, pad)) - let vend = vector.add(end, vector.scale(dir, pad + overshoot)) - let is-horizontal = name == "x" - - let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - group(name: "axis", { - _draw-axis-line(vstart, vend, axis, is-horizontal, style) - place-ticks-on-line(ticks, data-start, data-end, style, flip: flip) - }) - - if axis.label != none { - let (content-anchor, offset-dir) = label-pos.at(name) - - let angle = if style.label.angle not in (none, auto) { - style.label.angle - } else { 0deg } - if style.label.anchor not in (none, auto) { - content-anchor = style.label.anchor - } - - let offset = vector.scale(offset-dir, style.label.offset) - content((rel: offset, to: vend), - [#axis.label], - angle: angle, - anchor: content-anchor) - } - } - - if shared-zero { - let pt = (rel: (-style.tick.label.offset, -style.tick.label.offset), - to: (y-x, x-y)) - let zero = if type(style.shared-zero) == typst-content { - style.shared-zero - } else { - $0$ - } - content(pt, zero, anchor: "north-east") - } - }) - }) - }) -} diff --git a/src/lib/chart.typ b/src/lib/chart.typ deleted file mode 100644 index e7de11718..000000000 --- a/src/lib/chart.typ +++ /dev/null @@ -1,5 +0,0 @@ -// CeTZ Library for drawing charts -#import "chart/boxwhisker.typ": boxwhisker, boxwhisker-default-style -#import "chart/barchart.typ": barchart, barchart-default-style -#import "chart/columnchart.typ": columnchart, columnchart-default-style -#import "chart/piechart.typ": piechart, piechart-default-style diff --git a/src/lib/chart/barchart.typ b/src/lib/chart/barchart.typ deleted file mode 100644 index 69bc8c9de..000000000 --- a/src/lib/chart/barchart.typ +++ /dev/null @@ -1,142 +0,0 @@ -#import "/src/lib/palette.typ" -#import "/src/lib/plot.typ" -#import "/src/draw.typ" -#import "/src/styles.typ" - -#let barchart-default-style = ( - axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))), - bar-width: .8, - cluster-gap: 0, - error: ( - whisker-size: .25, - ), - y-inset: 1, -) - -/// Draw a bar chart. A bar chart is a chart that represents data with -/// rectangular bars that grow from left to right, proportional to the values -/// they represent. For examples see @barchart-examples. -/// -/// = Styling -/// *Root*: `barchart`. -/// #show-parameter-block("bar-width", "float", default: .8, [ -/// Width of a single bar (basic) or a cluster of bars (clustered) in the plot.]) -/// #show-parameter-block("y-inset", "float", default: 1, [ -/// Distance of the plot data to the plot's edges on the y-axis of the plot.]) -/// You can use any `plot` or `axes` related style keys, too. -/// -/// The `barchart` function is a wrapper of the `plot` API. Arguments passed -/// to `..plot-args` are passed to the `plot.plot` function. -/// -/// - data (array): Array of data rows. A row can be of type array or -/// dictionary, with `label-key` and `value-key` being -/// the keys to access a rows label and value(s). -/// -/// *Example* -/// ```typc -/// (([A], 1), ([B], 2), ([C], 3),) -/// ``` -/// - label-key (int,string): Key to access the label of a data row. -/// This key is used as argument to the -/// rows `.at(..)` function. -/// - value-key (int,string): Key(s) to access values of a data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - error-key (none,int,string): Key(s) to access error values of a data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - mode (string): Chart mode: -/// / basic: Single bar per data row -/// / clustered: Group of bars per data row -/// / stacked: Stacked bars per data row -/// / stacked100: Stacked bars per data row relative -/// to the sum of the row -/// - size (array): Chart size as width and height tuple in canvas unist; -/// width can be set to `auto`. -/// - bar-style (style,function): Style or function (idx => style) to use for -/// each bar, accepts a palette function. -/// - x-unit (content,auto): Tick suffix added to each tick label -/// - y-label (content,none): Y axis label -/// - x-label (content,none): x axis label -/// - labels (none,content): Legend labels per x value group -/// - ..plot-args (any): Arguments to pass to `plot.plot` -#let barchart(data, - label-key: 0, - value-key: 1, - error-key: none, - mode: "basic", - size: (auto, 1), - bar-style: palette.red, - x-label: none, - x-unit: auto, - y-label: none, - labels: none, - ..plot-args - ) = { - assert(type(label-key) in (int, str)) - if mode == "basic" { - assert(type(value-key) in (int, str)) - } else { - assert(type(value-key) == array) - } - - if type(value-key) != array { - value-key = (value-key,) - } - - if error-key == none { - error-key = () - } else if type(error-key) != array { - error-key = (error-key,) - } - - if type(size) != array { - size = (size, auto) - } - if size.at(1) == auto { - size.at(1) = (data.len() + 1) - } - - let y-tic-list = data.enumerate().map(((i, t)) => { - (data.len() - i - 1, t.at(label-key)) - }) - - let x-unit = x-unit - if x-unit == auto { - x-unit = if mode == "stacked100" {[%]} else [] - } - - data = data.enumerate().map(((i, d)) => { - (data.len() - i - 1, value-key.map(k => d.at(k, default: 0)).flatten(), error-key.map(k => d.at(k, default: 0)).flatten()) - }) - - draw.group(ctx => { - let style = styles.resolve(ctx.style, merge: (:), - root: "barchart", base: barchart-default-style) - draw.set-style(..style) - - let y-inset = calc.max(style.y-inset, style.bar-width / 2) - plot.plot(size: size, - axis-style: "scientific-auto", - x-label: x-label, - x-grid: true, - y-label: y-label, - y-min: -y-inset, - y-max: data.len() + y-inset - 1, - y-tick-step: none, - y-ticks: y-tic-list, - plot-style: bar-style, - ..plot-args, - { - plot.add-bar(data, - x-key: 0, - y-key: 1, - error-key: if mode in ("basic", "clustered") { 2 }, - mode: mode, - labels: labels, - bar-width: -style.bar-width, - cluster-gap: style.cluster-gap, - axes: ("y", "x")) - }) - }) -} diff --git a/src/lib/chart/barcol-common.typ b/src/lib/chart/barcol-common.typ deleted file mode 100644 index 0c09a5260..000000000 --- a/src/lib/chart/barcol-common.typ +++ /dev/null @@ -1,40 +0,0 @@ -// Valid bar- and columnchart modes -#let barchart-modes = ( - "basic", "clustered", "stacked", "stacked100" -) - -// Functions for max value calculation -#let barchart-max-value-fn = ( - basic: (data, value-key) => { - calc.max(0, ..data.map(t => t.at(value-key))) - }, - clustered: (data, value-key) => { - calc.max(0, ..data.map(t => calc.max( - ..value-key.map(k => t.at(k))))) - }, - stacked: (data, value-key) => { - calc.max(0, ..data.map(t => - value-key.map(k => t.at(k)).sum())) - }, - stacked100: (..) => { - 100 - } -) - -// Functions for min value calculation -#let barchart-min-value-fn = ( - basic: (data, value-key) => { - calc.min(0, ..data.map(t => t.at(value-key))) - }, - clustered: (data, value-key) => { - calc.min(0, ..data.map(t => calc.max( - ..value-key.map(k => t.at(k))))) - }, - stacked: (data, value-key) => { - calc.min(0, ..data.map(t => - value-key.map(k => t.at(k)).sum())) - }, - stacked100: (..) => { - 0 - } -) diff --git a/src/lib/chart/boxwhisker.typ b/src/lib/chart/boxwhisker.typ deleted file mode 100644 index 4ce18183d..000000000 --- a/src/lib/chart/boxwhisker.typ +++ /dev/null @@ -1,97 +0,0 @@ -#import "/src/lib/plot.typ" -#import "/src/draw.typ" -#import "/src/styles.typ" - -#let boxwhisker-default-style = ( - axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))), - box-width: 0.75, - whisker-width: 0.5, - mark-size: 0.15, -) - -/// Add one or more box or whisker plots. -/// -/// #example(``` -/// cetz.chart.boxwhisker(size: (2,2), label-key: none, -/// y-min: 0, y-max: 70, y-tick-step: 20, -/// (x: 1, min: 15, max: 60, -/// q1: 25, q2: 35, q3: 50)) -/// ```) -/// -/// = Styling -/// *Root* `boxwhisker` -/// #show-parameter-block("box-width", "float", default: .75, [ -/// The width of the box. Since boxes are placed 1 unit next to each other, -/// a width of $1$ would make neighbouring boxes touch.]) -/// #show-parameter-block("whisker-width", "float", default: .5, [ -/// The width of the whisker, that is the horizontal bar on the top and bottom -/// of the box.]) -/// #show-parameter-block("mark-size", "float", default: .15, [ -/// The scaling of the mark for the boxes outlier values in canvas units.]) -/// You can use any `plot` or `axes` related style keys, too. -/// -/// - data (array, dictionary): Dictionary or array of dictionaries containing the -/// needed entries to plot box and whisker plot. -/// -/// See `plot.add-boxwhisker` for more details. -/// -/// *Examples:* -/// - ```typc -/// (x: 1 // Location on x-axis -/// outliers: (7, 65, 69), // Optional outliers -/// min: 15, max: 60 // Minimum and maximum -/// q1: 25, // Quartiles: Lower -/// q2: 35, // Median -/// q3: 50) // Upper -/// ``` -/// - size (array) : Size of chart. If the second entry is auto, it automatically scales to accommodate the number of entries plotted -/// - label-key (integer, string): Index in the array where labels of each entry is stored -/// - mark (string): Mark to use for plotting outliers. Set `none` to disable. Defaults to "x" -/// - ..plot-args (any): Additional arguments are passed to `plot.plot` -#let boxwhisker(data, - size: (1, auto), - label-key: 0, - mark: "*", - ..plot-args - ) = { - if type(data) == dictionary { data = (data,) } - - if type(size) != array { - size = (size, auto) - } - if size.at(1) == auto { - size.at(1) = (data.len() + 1) - } - - let x-tick-list = data.enumerate().map(((i, t)) => { - (i + 1, if label-key != none { t.at(label-key, default: i) } else { [] }) - }) - - draw.group(ctx => { - let style = styles.resolve(ctx.style, merge: (:), - root: "boxwhisker", base: boxwhisker-default-style) - draw.set-style(..style) - - plot.plot( - size: size, - axis-style: "scientific-auto", - x-tick-step: none, - x-ticks: x-tick-list, - y-grid: true, - x-label: none, - y-label: none, - ..plot-args, - { - for (i, row) in data.enumerate() { - plot.add-boxwhisker( - (x: i + 1, ..row), - box-width: style.box-width, - whisker-width: style.whisker-width, - style: (:), - mark: mark, - mark-size: style.mark-size - ) - } - }) - }) -} diff --git a/src/lib/chart/columnchart.typ b/src/lib/chart/columnchart.typ deleted file mode 100644 index 3a1123173..000000000 --- a/src/lib/chart/columnchart.typ +++ /dev/null @@ -1,142 +0,0 @@ -#import "/src/lib/palette.typ" -#import "/src/lib/plot.typ" -#import "/src/draw.typ" -#import "/src/styles.typ" - -#let columnchart-default-style = ( - axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))), - bar-width: .8, - cluster-gap: 0, - error: ( - whisker-size: .25, - ), - x-inset: 1, -) - -/// Draw a column chart. A column chart is a chart that represents data with -/// rectangular bars that grow from bottom to top, proportional to the values -/// they represent. For examples see @columnchart-examples. -/// -/// = Styling -/// *Root*: `columnchart`. -/// #show-parameter-block("bar-width", "float", default: .8, [ -/// Width of a single bar (basic) or a cluster of bars (clustered) in the plot.]) -/// #show-parameter-block("x-inset", "float", default: 1, [ -/// Distance of the plot data to the plot's edges on the x-axis of the plot.]) -/// You can use any `plot` or `axes` related style keys, too. -/// -/// The `columnchart` function is a wrapper of the `plot` API. Arguments passed -/// to `..plot-args` are passed to the `plot.plot` function. -/// -/// - data (array): Array of data rows. A row can be of type array or -/// dictionary, with `label-key` and `value-key` being -/// the keys to access a rows label and value(s). -/// -/// *Example* -/// ```typc -/// (([A], 1), ([B], 2), ([C], 3),) -/// ``` -/// - label-key (int,string): Key to access the label of a data row. -/// This key is used as argument to the -/// rows `.at(..)` function. -/// - value-key (int,string): Key(s) to access value(s) of data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - error-key (none,int,string): Key(s) to access error values of a data row. -/// These keys are used as argument to the -/// rows `.at(..)` function. -/// - mode (string): Chart mode: -/// / basic: Single bar per data row -/// / clustered: Group of bars per data row -/// / stacked: Stacked bars per data row -/// / stacked100: Stacked bars per data row relative -/// to the sum of the row -/// - size (array): Chart size as width and height tuple in canvas unist; -/// width can be set to `auto`. -/// - bar-style (style,function): Style or function (idx => style) to use for -/// each bar, accepts a palette function. -/// - y-unit (content,auto): Tick suffix added to each tick label -/// - y-label (content,none): Y axis label -/// - x-label (content,none): x axis label -/// - labels (none,content): Legend labels per y value group -/// - ..plot-args (any): Arguments to pass to `plot.plot` -#let columnchart(data, - label-key: 0, - value-key: 1, - error-key: none, - mode: "basic", - size: (auto, 1), - bar-style: palette.red, - x-label: none, - y-unit: auto, - y-label: none, - labels: none, - ..plot-args - ) = { - assert(type(label-key) in (int, str)) - if mode == "basic" { - assert(type(value-key) in (int, str)) - } - - if type(value-key) != array { - value-key = (value-key,) - } - - if error-key == none { - error-key = () - } else if type(error-key) != array { - error-key = (error-key,) - } - - if type(size) != array { - size = (auto, size) - } - if size.at(0) == auto { - size.at(0) = (data.len() + 1) - } - - let x-tic-list = data.enumerate().map(((i, t)) => { - (i, t.at(label-key)) - }) - - let y-unit = y-unit - if y-unit == auto { - y-unit = if mode == "stacked100" {[%]} else [] - } - - data = data.enumerate().map(((i, d)) => { - (i, value-key.map(k => d.at(k)).flatten(), error-key.map(k => d.at(k, default: 0)).flatten()) - }) - - draw.group(ctx => { - let style = styles.resolve(ctx.style, merge: (:), - root: "columnchart", base: columnchart-default-style) - draw.set-style(..style) - - let x-inset = calc.max(style.x-inset, style.bar-width / 2) - plot.plot(size: size, - axis-style: "scientific-auto", - y-grid: true, - y-label: y-label, - x-min: -x-inset, - x-max: data.len() + x-inset - 1, - x-tick-step: none, - x-ticks: x-tic-list, - x-label: x-label, - plot-style: bar-style, - ..plot-args, - { - plot.add-bar(data, - x-key: 0, - y-key: 1, - error-key: if mode in ("basic", "clustered") { 2 }, - mode: mode, - labels: labels, - bar-width: style.bar-width, - cluster-gap: style.cluster-gap, - error-style: style.error, - whisker-size: style.error.whisker-size, - axes: ("x", "y")) - }) - }) -} diff --git a/src/lib/chart/piechart.typ b/src/lib/chart/piechart.typ deleted file mode 100644 index 48e90ab05..000000000 --- a/src/lib/chart/piechart.typ +++ /dev/null @@ -1,495 +0,0 @@ -#import "/src/draw.typ" -#import "/src/styles.typ" -#import "/src/intersection.typ" -#import "/src/vector.typ" -#import "/src/util.typ": circle-arclen -#import "/src/lib/plot/legend.typ" -#import "/src/lib/palette.typ" - -// Piechart Label Kind -#let label-kind = (value: "VALUE", percentage: "%", label: "LABEL") - -// Piechart Default Style -#let default-style = ( - stroke: auto, - fill: auto, - /// Outer chart radius - radius: 1, - /// Inner slice radius - inner-radius: 0, - /// Gap between items. This can be a canvas length or an angle - gap: 0.5deg, - /// Outset offset, absolute or relative to radius - outset-offset: 10%, - /// Pie outset mode: - /// - "OFFSET": Offset slice position by outset-offset - /// - "RADIUS": Offset slice radius by outset-offset (the slice gets scaled) - outset-mode: "OFFSET", - /// Pie start angle - start: 90deg, - /// Pie stop angle - stop: 360deg + 90deg, - /// Pie rotation direction (true = clockwise, false = anti-clockwise) - clockwise: true, - outer-label: ( - /// Label kind - /// If set to a function, that function gets called with (value, label) of each item - content: label-kind.label, - /// Absolute radius or percentage of radius - radius: 125%, - /// Absolute angle or auto to use secant of the slice as direction - angle: 0deg, - /// Label anchor - anchor: "center", - ), - inner-label: ( - /// Label kind - /// If set to a function, that function gets called with (value, label) of each item - content: none, - /// Absolute radius or percentage of the mid between radius and inner-radius - radius: 150%, - /// Absolute angle or auto to use secant of the slice as direction - angle: 0deg, - /// Label anchor - anchor: "center", - ), - legend: ( - ..legend.default-style, - - /// Label used for the legend - /// The legend gets rendered as soon as at least one item with a label - /// exists and the `legend-label.content` is set != none. This field - /// accepts the same values as inner-label.content or outer-label.content. - label: "LABEL", - - /// Anchor of the charts data bounding box to place the legend relative to - position: "south", - - /// Anchor of the legend bounding box to use as origin - anchor: "north", - - /// Custom preview function override - /// The function takes an item dictionary an is responsible for drawing - /// the preview icon. Stroke and fill styles are set to match the items - /// style. - preview: none, - - /// See lenged.typ for the following style keys - orientation: ltr, - offset: (0,-.5em), - stroke: none, - item: ( - spacing: .25, - preview: ( - width: .3, - height: .3, - ), - ), - ) -) -#let piechart-default-style = default-style - - -/// Draw a pie- or donut-chart -/// -/// #example(``` -/// import cetz.chart -/// let data = (24, 31, 18, 21, 23, 18, 27, 17, 26, 13) -/// let colors = gradient.linear(red, blue, green, yellow) -/// -/// chart.piechart( -/// data, -/// radius: 1.5, -/// slice-style: colors, -/// inner-radius: .5, -/// outer-label: (content: "%",)) -/// ```) -/// -/// = Styling -/// *Root* `piechart` \ -/// #show-parameter-block("radius", ("number"), [ -/// Outer radius of the chart.], default: 1) -/// #show-parameter-block("inner-radius", ("number"), [ -/// Inner radius of the chart slices. If greater than zero, the chart becomes -/// a "donut-chart".], default: 0) -/// #show-parameter-block("gap", ("number", "angle"), [ -/// Gap between chart slices to leave empty. This does not increase the charts -/// radius by pushing slices outwards, but instead shrinks the slice. Big -/// values can result in slices becoming invisible if no space is left.], default: 0.5deg) -/// #show-parameter-block("outset-offset", ("number", "ratio"), [ -/// Absolute, or radius relative distance to push slices marked for -/// "outsetting" outwards from the center of the chart.], default: 10%) -/// #show-parameter-block("outset-offset", ("string"), [ -/// The mode of how to perform "outsetting" of slices: -/// - "OFFSET": Offset slice position by `outset-offset`, increasing their gap to their siblings -/// - "RADIUS": Offset slice radius by `outset-offset`, which scales the slice and leaves the gap unchanged], default: "OFFSET") -/// #show-parameter-block("start", ("angle"), [ -/// The pie-charts start angle (ccw). You can use this to draw charts not forming a full circle.], default: 90deg) -/// #show-parameter-block("stop", ("angle"), [ -/// The pie-charts stop angle (ccw).], default: 360deg + 90deg) -/// #show-parameter-block("clockwise", ("bool"), [ -/// The pie-charts rotation direction.], default: true) -/// #show-parameter-block("outer-label.content", ("none","string","function"), [ -/// Content to display outsides the charts slices. -/// There are the following predefined values: -/// / LABEL: Display the slices label (see `label-key`) -/// / %: Display the percentage of the items value in relation to the sum of -/// all values, rounded to the next integer -/// / VALUE: Display the slices value -/// If passed a `` of the format `(value, label) => content`, -/// that function gets called with each slices value and label and must return -/// content, that gets displayed.], default: "LABEL") -/// #show-parameter-block("outer-label.radius", ("number","ratio"), [ -/// Absolute, or radius relative distance from the charts center to position -/// outer labels at.], default: 125%) -/// #show-parameter-block("outer-label.angle", ("angle","auto"), [ -/// The angle of the outer label. If passed `auto`, the label gets rotated, -/// so that the baseline is parallel to the slices secant. ], default: 0deg) -/// #show-parameter-block("outer-label.anchor", ("string"), [ -/// The anchor of the outer label to use for positioning.], default: "center") -/// #show-parameter-block("inner-label.content", ("none","string","function"), [ -/// Content to display insides the charts slices. -/// See `outer-label.content` for the possible values.], default: none) -/// #show-parameter-block("inner-label.radius", ("number","ratio"), [ -/// Distance of the inner label to the charts center. If passed a ``, -/// that ratio is relative to the mid between the inner and outer radius (`inner-radius` and `radius`) -/// of the chart], default: 150%) -/// #show-parameter-block("inner-label.angle", ("angle","auto"), [ -/// See `outer-label.angle`.], default: 0deg) -/// #show-parameter-block("inner-label.anchor", ("string"), [ -/// See `outer-label.anchor`.], default: "center") -/// #show-parameter-block("legend.label", ("none","string","function"), [ -/// See `outer-label.content`. The legend gets shown if this key is set != none.], default: "LABEL") -/// -/// = Anchors -/// The chart places one anchor per item at the radius of it's slice that -/// gets named `"item-"` (outer radius) and `"item--inner"` (inner radius), -/// where index is the index of the sclice data in `data`. -/// -/// - data (array): Array of data items. A data item can be: -/// - A number: A number that is used as the fraction of the slice -/// - An array: An array which is read depending on value-key, label-key and outset-key -/// - A dictionary: A dictionary which is read depending on value-key, label-key and outset-key -/// - value-key (none,int,string): Key of the "value" of a data item. If for example -/// data items are passed as dictionaries, the value-key is the key of the dictionary to -/// access the items chart value. -/// - label-key (none,int,string): Same as the value-key but for getting an items label content. -/// - outset-key (none,int,string): Same as the value-key but for getting if an item should get outset (highlighted). The -/// outset can be a bool, float or ratio. If of type `bool`, the outset distance from the -/// style gets used. -/// - outset (none,int,array): A single or multiple indices of items that should get offset from the center to the outsides -/// of the chart. Only used if outset-key is none! -/// - slice-style (function,array,gradient): Slice style of the following types: -/// - function: A function of the form `index => style` that must return a style dictionary. -/// This can be a `palette` function. -/// - array: An array of style dictionaries or fill colors of at least one item. For each slice the style at the slices -/// index modulo the arrays length gets used. -/// - gradient: A gradient that gets sampled for each data item using the the slices -/// index divided by the number of slices as position on the gradient. -/// If one of stroke or fill is not in the style dictionary, it is taken from the charts style. -#let piechart(data, - value-key: none, - label-key: none, - outset-key: none, - outset: none, - slice-style: palette.red, - name: none, - ..style) = { - import draw: * - - // Prepare data by converting it to tuples of the format - // (value, label, outset) - data = data.enumerate().map(((i, item)) => ( - if value-key != none { - item.at(value-key) - } else { - item - }, - if label-key != none { - item.at(label-key) - } else { - none - }, - if outset-key != none { - item.at(outset-key, default: false) - } else if outset != none { - i == outset or (type(outset) == array and i in outset) - } else { - false - } - )) - - let sum = data.map(((value, ..)) => value).sum() - if sum == 0 { - sum = 1 - } - - group(name: name, ctx => { - anchor("default", (0,0)) - - let style = styles.resolve(ctx, - merge: style.named(), root: "piechart", base: default-style) - - let gap = style.gap - if type(gap) != angle { - gap = gap / (2 * calc.pi * style.radius) * 360deg - } - assert(gap < 360deg / data.len(), - message: "Gap angle is too big for " + str(data.len()) + "items. Maximum gap angle: " + repr(360deg / data.len())) - - let radius = style.radius - assert(radius > 0, - message: "Radius must be > 0.") - - let inner-radius = style.inner-radius - assert(inner-radius >= 0 and inner-radius <= radius, - message: "Radius must be >= 0 and <= radius.") - - assert(style.outset-mode in ("OFFSET", "RADIUS"), - message: "Outset mode must be 'OFFSET' or 'RADIUS', but is: " + str(style.outset-mode)) - - let style-at = if type(slice-style) == function { - slice-style - } else if type(slice-style) == array { - i => { - let s = slice-style.at(calc.rem(i, slice-style.len())) - if type(s) == color { - (fill: s) - } else { - s - } - } - } else if type(slice-style) == gradient { - i => (fill: slice-style.sample(i / data.len() * 100%)) - } - - let start-angle = style.start - let stop-angle = style.stop - let f = (stop-angle - start-angle) / sum - - let get-item-label(item, kind) = { - let (value, label, ..) = item - if kind == label-kind.value { - [#value] - } else if kind == label-kind.percentage { - [#{calc.round(value / sum * 100)}%] - } else if kind == label-kind.label { - label - } else if type(kind) == function { - (kind)(value, label) - } - } - - let start = start-angle - let enum-items = if style.clockwise { - data.enumerate().rev() - } else { - data.enumerate() - } - group(name: "chart", { - for (i, item) in enum-items { - let (value, label, outset) = item - if value == 0 { continue } - - let origin = (0,0) - let radius = radius - let inner-radius = inner-radius - - // Calculate item angles - let delta = f * value - let end = start + delta - - // Apply item outset - let outset-offset = if outset == true { - style.outset-offset - } else if outset == false { - 0 - } else if type(outset) in (float, ratio) { - outset - } else { - panic("Invalid type for outset. Expected bool, float or ratio, got: " + repr(outset)) - } - if type(outset-offset) == ratio { - outset-offset = outset-offset * radius / 100% - } - - if outset-offset != 0 { - if style.outset-mode == "OFFSET" { - let dir = (calc.cos((start + end) / 2), calc.sin((start + end) / 2)) - origin = vector.add(origin, vector.scale(dir, outset-offset)) - radius += outset-offset - } else { - radius += outset-offset - if inner-radius > 0 { - inner-radius += outset-offset - } - } - } - - // Calculate gap angles - let outer-gap = gap - let gap-dist = outer-gap / 360deg * 2 * calc.pi * radius - let inner-gap = if inner-radius > 0 { - gap-dist / (2 * calc.pi * inner-radius) * 360deg - } else { - 1 / calc.pi * 360deg - } - - // Calculate angle deltas - let outer-angle = end - start - outer-gap * 2 - let inner-angle = end - start - inner-gap * 2 - let mid-angle = (start + end) / 2 - - // Skip negative values - if outer-angle < 0deg { - // TODO: Add a warning as soon as Typst is ready! - continue - } - - // A sharp item is an item that should be round but is sharp due to the gap being big - let is-sharp = inner-radius == 0 or circle-arclen(inner-radius, angle: inner-angle) > circle-arclen(radius, angle: outer-angle) - - let inner-origin = vector.add(origin, if inner-radius == 0 { - if gap-dist >= 0 { - let outer-end = vector.scale((calc.cos(end - outer-gap), calc.sin(end - outer-gap)), radius) - let inner-end = vector.scale((calc.cos(end - inner-gap), calc.sin(end - inner-gap)), gap-dist) - let outer-start = vector.scale((calc.cos(start + outer-gap), calc.sin(start + outer-gap)), radius) - let inner-start = vector.scale((calc.cos(start + inner-gap), calc.sin(start + inner-gap)), gap-dist) - - intersection.line-line(outer-end, inner-end, outer-start, inner-start, ray: true) - } else { - (0,0) - } - } else if is-sharp { - let outer-end = vector.scale((calc.cos(end - outer-gap), calc.sin(end - outer-gap)), radius) - let inner-end = vector.scale((calc.cos(end - inner-gap), calc.sin(end - inner-gap)), inner-radius) - let outer-start = vector.scale((calc.cos(start + outer-gap), calc.sin(start + outer-gap)), radius) - let inner-start = vector.scale((calc.cos(start + inner-gap), calc.sin(start + inner-gap)), inner-radius) - - intersection.line-line(outer-end, inner-end, outer-start, inner-start, ray: true) - } else { - (0,0) - }) - - // Draw one segment - let stroke = style-at(i).at("stroke", default: style.stroke) - let fill = style-at(i).at("fill", default: style.fill) - if data.len() == 1 { - // If the chart has only one segment, we may have to fake a path - // with a hole in it by using a combination of multiple arcs. - if inner-radius > 0 { - // Split the circle/arc into two arcs - // and fill them - merge-path({ - arc(origin, start: start-angle, stop: mid-angle, radius: radius, anchor: "origin") - arc(origin, stop: start-angle, start: mid-angle, radius: inner-radius, anchor: "origin") - }, close: false, fill:fill, stroke: none) - merge-path({ - arc(origin, start: mid-angle, stop: stop-angle, radius: radius, anchor: "origin") - arc(origin, stop: mid-angle, start: stop-angle, radius: inner-radius, anchor: "origin") - }, close: false, fill:fill, stroke: none) - - // Create arcs for the inner and outer border and stroke them. - // If the chart is not a full circle, we have to merge two arc - // at their ends to create closing lines - if stroke != none { - if calc.abs(stop-angle - start-angle) != 360deg { - merge-path({ - arc(origin, start: start, stop: end, radius: inner-radius, anchor: "origin") - arc(origin, start: end, stop: start, radius: radius, anchor: "origin") - }, close: true, fill: none, stroke: stroke) - } else { - arc(origin, start: start, stop: end, radius: inner-radius, fill: none, stroke: stroke, anchor: "origin") - arc(origin, start: start, stop: end, radius: radius, fill: none, stroke: stroke, anchor: "origin") - } - } - } else { - arc(origin, start: start, stop: end, radius: radius, fill: fill, stroke: stroke, mode: "PIE", anchor: "origin") - } - } else { - // Draw a normal segment - if inner-origin != none { - merge-path({ - arc(origin, start: start + outer-gap, stop: end - outer-gap, anchor: "origin", - radius: radius) - if inner-radius > 0 and not is-sharp { - if inner-angle < 0deg { - arc(inner-origin, stop: end - inner-gap, delta: inner-angle, anchor: "origin", - radius: inner-radius) - } else { - arc(inner-origin, start: end - inner-gap, delta: -inner-angle, anchor: "origin", - radius: inner-radius) - } - } else { - line((rel: (end - outer-gap, radius), to: origin), - inner-origin, - (rel: (start + outer-gap, radius), to: origin)) - } - }, close: true, fill: fill, stroke: stroke) - } - } - - // Place outer label - let outer-label = get-item-label(item, style.outer-label.content) - if outer-label != none { - let r = style.outer-label.radius - if type(r) == ratio {r = r * radius / 100%} - - let dir = (r * calc.cos(mid-angle), r * calc.sin(mid-angle)) - let pt = vector.add(origin, dir) - - let angle = style.outer-label.angle - if angle == auto { - angle = vector.add(pt, (dir.at(1), -dir.at(0))) - } - - content(pt, outer-label, angle: angle, anchor: style.outer-label.anchor) - } - - // Place inner label - let inner-label = get-item-label(item, style.inner-label.content) - if inner-label != none { - let r = style.inner-label.radius - if type(r) == ratio {r = r * (radius + inner-radius) / 200%} - - let dir = (r * calc.cos(mid-angle), r * calc.sin(mid-angle)) - let pt = vector.add(origin, dir) - - let angle = style.inner-label.angle - if angle == auto { - angle = vector.add(pt, (dir.at(1), -dir.at(0))) - } - - content(pt, inner-label, angle: angle, anchor: style.inner-label.anchor) - } - - // Place item anchor - anchor("item-" + str(i), (rel: (mid-angle, radius), to: origin)) - anchor("item-" + str(i) + "-inner", (rel: (mid-angle, inner-radius), to: origin)) - - start = end - } - }) - - legend.legend((name: "chart", anchor: style.legend.position), { - let preview-fn = if style.legend.preview != none { - style.legend.preview - } else { - (_) => { rect((0,0), (1,1)) } - } - - for (i, item) in enum-items.rev() { - let label = get-item-label(item, style.legend.label) - let preview = (item) => { - let stroke = style-at(i).at("stroke", default: style.stroke) - let fill = style-at(i).at("fill", default: style.fill) - - set-style(stroke: stroke, fill: fill) - preview-fn(item) - } - - legend.item(label, preview) - } - }, ..style.at("legend", default: (:))) - }) -} diff --git a/src/lib/plot.typ b/src/lib/plot.typ deleted file mode 100644 index 04ff46141..000000000 --- a/src/lib/plot.typ +++ /dev/null @@ -1,536 +0,0 @@ -// CeTZ Library for drawing plots -#import "/src/util.typ" -#import "/src/draw.typ" -#import "/src/matrix.typ" -#import "/src/vector.typ" -#import "/src/bezier.typ" -#import "/src/styles.typ" -#import "axes.typ" -#import "palette.typ" - -#import "plot/sample.typ": sample-fn, sample-fn2 -#import "plot/line.typ": add, add-hline, add-vline, add-fill-between -#import "plot/contour.typ": add-contour -#import "plot/boxwhisker.typ": add-boxwhisker -#import "plot/util.typ" as plot-util -#import "plot/legend.typ" as plot-legend -#import "plot/annotation.typ": annotate, calc-annotation-domain -#import "plot/bar.typ": add-bar -#import "plot/errorbar.typ": add-errorbar -#import "plot/mark.typ" - -#let default-colors = (blue, red, green, yellow, black) - -#let default-plot-style(i) = { - let color = default-colors.at(calc.rem(i, default-colors.len())) - return (stroke: color, - fill: color.lighten(75%)) -} - -#let default-mark-style(i) = { - return default-plot-style(i) -} - -/// Create a plot environment. Data to be plotted is given by passing it to the -/// `plot.add` or other plotting functions. The plot environment supports different -/// axis styles to draw, see its parameter `axis-style:`. -/// -/// #example(``` -/// import cetz.plot -/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// plot.add(((0,0), (1,1), (2,.5), (4,3))) -/// }) -/// ```) -/// -/// To draw elements insides a plot, using the plots coordinate system, use -/// the `plot.annotate(..)` function. -/// -/// = parameters -/// -/// = Options -/// -/// You can use the following options to customize each axis of the plot. You must pass them as named arguments prefixed by the axis name followed by a dash (`-`) they should target. Example: `x-min: 0`, `y-ticks: (..)` or `x2-label: [..]`. -/// -/// #show-parameter-block("label", ("none", "content"), default: "none", [ -/// The axis' label. If and where the label is drawn depends on the `axis-style`.]) -/// #show-parameter-block("min", ("auto", "float"), default: "auto", [ -/// Axis lower domain value. If this is set greater than than `max`, the axis' direction is swapped]) -/// #show-parameter-block("max", ("auto", "float"), default: "auto", [ -/// Axis upper domain value. If this is set to a lower value than `min`, the axis' direction is swapped]) -/// #show-parameter-block("equal", ("string"), default: "none", [ -/// Set the axis domain to keep a fixed aspect ratio by multiplying the other axis domain by the plots aspect ratio, -/// depending on the other axis orientation (see `horizontal`). -/// This can be useful to force one axis to grow or shrink with another one. -/// You can only "lock" two axes of different orientations. -/// #example(``` -/// cetz.plot.plot(size: (2,1), x-tick-step: 1, y-tick-step: 1, -/// x-equal: "y", -/// { -/// cetz.plot.add(domain: (0, 2 * calc.pi), -/// t => (calc.cos(t), calc.sin(t))) -/// }) -/// ```) -/// ]) -/// #show-parameter-block("horizontal", ("bool"), default: "axis name dependant", [ -/// If true, the axis is considered an axis that gets drawn horizontally, vertically otherwise. -/// The default value depends on the axis name on axis creation. Axes which name start with `x` have this -/// set to `true`, all others have it set to `false`. Each plot has to use one horizontal and one -/// vertical axis for plotting, a combination of two y-axes will panic: ("y", "y2"). -/// ]) -/// #show-parameter-block("tick-step", ("none", "auto", "float"), default: "auto", [ -/// The increment between tick marks on the axis. If set to `auto`, an -/// increment is determined. When set to `none`, incrementing tick marks are disabled.]) -/// #show-parameter-block("minor-tick-step", ("none", "float"), default: "none", [ -/// Like `tick-step`, but for minor tick marks. In contrast to ticks, minor ticks do not have labels.]) -/// #show-parameter-block("ticks", ("none", "array"), default: "none", [ -/// A List of custom tick marks to additionally draw along the axis. They can be passed as -/// an array of `` values or an array of `(, )` tuples for -/// setting custom tick mark labels per mark. -/// -/// #example(``` -/// cetz.plot.plot(x-tick-step: none, y-tick-step: none, -/// x-min: 0, x-max: 4, -/// x-ticks: (1, 2, 3), -/// y-min: 1, y-max: 2, -/// y-ticks: ((1, [One]), (2, [Two])), -/// { -/// cetz.plot.add(((0,0),)) -/// }) -/// ```) -/// -/// Examples: `(1, 2, 3)` or `((1, [One]), (2, [Two]), (3, [Three]))`]) -/// #show-parameter-block("format", ("none", "string", "function"), default: "float", [ -/// How to format the tick label: You can give a function that takes a `` and return -/// `` to use as the tick label. You can also give one of the predefined options: -/// / float: Floating point formatting rounded to two digits after the point (see `decimals`) -/// / sci: Scientific formatting with $times 10^n$ used as exponet syntax -/// -/// #example(``` -/// let formatter(v) = if v != 0 {$ #{v/calc.pi} pi $} else {$ 0 $} -/// cetz.plot.plot(x-tick-step: calc.pi, y-tick-step: none, -/// x-min: 0, x-max: 2 * calc.pi, -/// x-format: formatter, -/// { -/// cetz.plot.add(((0,0),)) -/// }) -/// ```) -/// ]) -/// #show-parameter-block("decimals", ("int"), default: "2", [ -/// Number of decimals digits to display for tick labels, if the format is set -/// to `"float"`. -/// ]) -/// #show-parameter-block("unit", ("none", "content"), default: "none", [ -/// Suffix to append to all tick labels. -/// ]) -/// #show-parameter-block("grid", ("bool", "string"), default: "false", [ -/// If `true` or `"major"`, show grid lines for all major ticks. If set -/// to `"minor"`, show grid lines for minor ticks only. -/// The value `"both"` enables grid lines for both, major- and minor ticks. -/// -/// #example(``` -/// cetz.plot.plot(x-tick-step: 1, y-tick-step: 1, -/// y-minor-tick-step: .2, -/// x-min: 0, x-max: 2, x-grid: true, -/// y-min: 0, y-max: 2, y-grid: "both", { -/// cetz.plot.add(((0,0),)) -/// }) -/// ```) -/// ]) -/// #show-parameter-block("break", ("bool"), default: "false", [ -/// If true, add a "sawtooth" at the start or end of the axis line, depending -/// on the axis bounds. If the axis min. value is > 0, a sawtooth is added -/// to the start of the axes, if the axis max. value is < 0, a sawtooth is added -/// to its end.]) -/// -/// - body (body): Calls of `plot.add` or `plot.add-*` commands. Note that normal drawing -/// commands like `line` or `rect` are not allowed inside the plots body, instead wrap -/// them in `plot.annotate`, which lets you select the axes used for drawing. -/// - size (array): Plot size tuple of `(, )` in canvas units. -/// This is the plots inner plotting size without axes and labels. -/// - axis-style (none, string): How the axes should be styled: -/// / scientific: Frames plot area using a rectangle and draw axes `x` (bottom), `y` (left), `x2` (top), and `y2` (right) around it. -/// If `x2` or `y2` are unset, they mirror their opposing axis. -/// / scientific-auto: Draw set (used) axes `x` (bottom), `y` (left), `x2` (top) and `y2` (right) around -/// the plotting area, forming a rect if all axes are in use or a L-shape if only `x` and `y` are in use. -/// / school-book: Draw axes `x` (horizontal) and `y` (vertical) as arrows pointing to the right/top with both crossing at $(0, 0)$ -/// / left: Draw axes `x` and `y` as arrows, while the y axis stays on the left (at `x.min`) -/// and the x axis at the bottom (at `y.min`) -/// / `none`: Draw no axes (and no ticks). -/// -/// #example(``` -/// let opts = (x-tick-step: none, y-tick-step: none, size: (2,1)) -/// let data = cetz.plot.add(((-1,-1), (1,1),), mark: "o") -/// -/// for name in (none, "school-book", "left", "scientific") { -/// cetz.plot.plot(axis-style: name, ..opts, data, name: "plot") -/// content(((0,-1), "-|", "plot.south"), repr(name)) -/// set-origin((3.5,0)) -/// } -/// ```, vertical: true) -/// - plot-style (style,function): Styling to use for drawing plot graphs. -/// This style gets inherited by all plots and supports `palette` functions. -/// The following style keys are supported: -/// #show-parameter-block("stroke", ("none", "stroke"), default: 1pt, [ -/// Stroke style to use for stroking the graph. -/// ]) -/// #show-parameter-block("fill", ("none", "paint"), default: none, [ -/// Paint to use for filled graphs. Note that not all graphs may support filling and -/// that you may have to enable filling per graph, see `plot.add(fill: ..)`. -/// ]) -/// - mark-style (style,function): Styling to use for drawing plot marks. -/// This style gets inherited by all plots and supports `palette` functions. -/// The following style keys are supported: -/// #show-parameter-block("stroke", ("none", "stroke"), default: 1pt, [ -/// Stroke style to use for stroking the mark. -/// ]) -/// #show-parameter-block("fill", ("none", "paint"), default: none, [ -/// Paint to use for filling marks. -/// ]) -/// - fill-below (bool): If true, the filled shape of plots is drawn _below_ axes. -/// - name (string): The plots element name to be used when referring to anchors -/// - legend (none, auto, coordinate): The position the legend will be drawn at. See @plot-legends for information about legends. If set to ``, the legend's "default-placement" styling will be used. If set to a ``, it will be taken as relative to the plot's origin. -/// - legend-anchor (auto, string): Anchor of the legend group to use as its origin. -/// If set to `auto` and `lengend` is one of the predefined legend anchors, the -/// opposite anchor to `legend` gets used. -/// - legend-style (style): Style key-value overwrites for the legend style with style root `legend`. -/// - ..options (any): Axis options, see _options_ below. -#let plot(body, - size: (1, 1), - axis-style: "scientific", - name: none, - plot-style: default-plot-style, - mark-style: default-mark-style, - fill-below: true, - legend: auto, - legend-anchor: auto, - legend-style: (:), - ..options - ) = draw.group(name: name, ctx => { - // Create plot context object - let make-ctx(x, y, size) = { - assert(x != none, message: "X axis does not exist") - assert(y != none, message: "Y axis does not exist") - assert(size.at(0) > 0 and size.at(1) > 0, message: "Plot size must be > 0") - - let x-scale = ((x.max - x.min) / size.at(0)) - let y-scale = ((y.max - y.min) / size.at(1)) - - if y.horizontal { - (x-scale, y-scale) = (y-scale, x-scale) - } - - return (x: x, y: y, size: size, x-scale: x-scale, y-scale: y-scale) - } - - // Setup data viewport - let data-viewport(data, x, y, size, body, name: none) = { - if body == none or body == () { return } - - assert.ne(x.horizontal, y.horizontal, - message: "Data must use one horizontal and one vertical axis!") - - // If y is the horizontal axis, swap x and y - // coordinates by swapping the transformation - // matrix columns. - if y.horizontal { - (x, y) = (y, x) - body = draw.set-ctx(ctx => { - ctx.transform = matrix.swap-cols(ctx.transform, 0, 1) - return ctx - }) + body - } - - // Setup the viewport - axes.axis-viewport(size, x, y, body, name: name) - } - - let data = () - let anchors = () - let annotations = () - let body = if body != none { body } else { () } - - for cmd in body { - assert(type(cmd) == dictionary and "type" in cmd, - message: "Expected plot sub-command in plot body") - if cmd.type == "anchor" { - anchors.push(cmd) - } else if cmd.type == "annotation" { - annotations.push(cmd) - } else { data.push(cmd) } - } - - assert(axis-style in (none, "scientific", "scientific-auto", "school-book", "left"), - message: "Invalid plot style") - - // Create axes for data & annotations - let axis-dict = (:) - for d in data + annotations { - for (i, name) in d.axes.enumerate() { - if not name in axis-dict { - axis-dict.insert(name, axes.axis( - min: none, max: none)) - } - - let axis = axis-dict.at(name) - let domain = if i == 0 { - d.at("x-domain", default: (0, 0)) - } else { - d.at("y-domain", default: (0, 0)) - } - if domain != (none, none) { - axis.min = util.min(axis.min, ..domain) - axis.max = util.max(axis.max, ..domain) - } - - axis-dict.at(name) = axis - } - } - - // Create axes for anchors - for a in anchors { - for (i, name) in a.axes.enumerate() { - if not name in axis-dict { - axis-dict.insert(name, axes.axis(min: none, max: none)) - } - } - } - - // Adjust axis bounds for annotations - for a in annotations { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - (x, y) = calc-annotation-domain(ctx, x, y, a) - axis-dict.at(a.axes.at(0)) = x - axis-dict.at(a.axes.at(1)) = y - } - - // Set axis options - axis-dict = plot-util.setup-axes(ctx, axis-dict, options.named(), size) - - // Prepare styles - for i in range(data.len()) { - let style-base = plot-style - if type(style-base) == function { - style-base = (style-base)(i) - } - assert.eq(type(style-base), dictionary, - message: "plot-style must be of type dictionary") - - if type(data.at(i).style) == function { - data.at(i).style = (data.at(i).style)(i) - } - assert.eq(type(style-base), dictionary, - message: "data plot-style must be of type dictionary") - - data.at(i).style = util.merge-dictionary( - style-base, data.at(i).style) - - if "mark-style" in data.at(i) { - let mark-style-base = mark-style - if type(mark-style-base) == function { - mark-style-base = (mark-style-base)(i) - } - assert.eq(type(mark-style-base), dictionary, - message: "mark-style must be of type dictionary") - - if type(data.at(i).mark-style) == function { - data.at(i).mark-style = (data.at(i).mark-style)(i) - } - - if type(data.at(i).mark-style) == dictionary { - data.at(i).mark-style = util.merge-dictionary( - mark-style-base, - data.at(i).mark-style - ) - } - } - } - - draw.group(name: "plot", { - draw.anchor("origin", (0, 0)) - - // Prepare - for i in range(data.len()) { - let (x, y) = data.at(i).axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) - - if "plot-prepare" in data.at(i) { - data.at(i) = (data.at(i).plot-prepare)(data.at(i), plot-ctx) - assert(data.at(i) != none, - message: "Plot prepare(self, cxt) returned none!") - } - } - - // Background Annotations - for a in annotations.filter(a => a.background) { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) - - data-viewport(a, x, y, size, { - draw.anchor("default", (0, 0)) - a.body - }) - } - - // Fill - if fill-below { - for d in data { - let (x, y) = d.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) - - data-viewport(d, x, y, size, { - draw.anchor("default", (0, 0)) - draw.set-style(..d.style) - - if "plot-fill" in d { - (d.plot-fill)(d, plot-ctx) - } - }) - } - } - - if axis-style in ("scientific", "scientific-auto") { - let draw-unset = if axis-style == "scientific" { - true - } else { - false - } - - let mirror = if axis-style == "scientific" { - auto - } else { - none - } - - axes.scientific( - size: size, - draw-unset: draw-unset, - bottom: axis-dict.at("x", default: none), - top: axis-dict.at("x2", default: mirror), - left: axis-dict.at("y", default: none), - right: axis-dict.at("y2", default: mirror),) - } else if axis-style == "left" { - axes.school-book( - size: size, - axis-dict.x, - axis-dict.y, - x-position: axis-dict.y.min, - y-position: axis-dict.x.min) - } else if axis-style == "school-book" { - axes.school-book( - size: size, - axis-dict.x, - axis-dict.y,) - } - - // Stroke + Mark data - for d in data { - let (x, y) = d.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) - - data-viewport(d, x, y, size, { - draw.anchor("default", (0, 0)) - draw.set-style(..d.style) - - if not fill-below and "plot-fill" in d { - (d.plot-fill)(d, plot-ctx) - } - if "plot-stroke" in d { - (d.plot-stroke)(d, plot-ctx) - } - if "mark" in d and d.mark != none { - draw.set-style(..d.style, ..d.mark-style) - mark.draw-mark(d.data, x, y, d.mark, d.mark-size, size) - } - }) - } - - // Foreground Annotations - for a in annotations.filter(a => not a.background) { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) - - data-viewport(a, x, y, size, { - draw.anchor("default", (0, 0)) - a.body - }) - } - - // Place anchors - for a in anchors { - let (x, y) = a.axes.map(name => axis-dict.at(name)) - let plot-ctx = make-ctx(x, y, size) - - data-viewport(a, x, y, size, { - let (ax, ay) = a.position - if ax == "min" {ax = x.min} else if ax == "max" {ax = x.max} - if ay == "min" {ay = y.min} else if ay == "max" {ay = y.max} - draw.anchor("default", (0,0)) - draw.anchor(a.name, (ax, ay)) - }, name: "anchors") - draw.copy-anchors("anchors", filter: (a.name,)) - } - }) - - // Draw the legend - if legend != none { - let items = data.filter(d => "label" in d and d.label != none) - if items.len() > 0 { - let legend-style = styles.resolve(ctx.style, - base: plot-legend.default-style, merge: legend-style, root: "legend") - - plot-legend.add-legend-anchors(legend-style, "plot", size) - plot-legend.legend(legend, anchor: legend-anchor, { - for item in items { - let preview = if "plot-legend-preview" in item { - _ => {(item.plot-legend-preview)(item) } - } else { - auto - } - - plot-legend.item(item.label, preview, - mark: item.at("mark", default: none), - mark-size: item.at("mark-size", default: none), - mark-style: item.at("mark-style", default: none), - ..item.style) - } - }, ..legend-style) - } - } - - draw.copy-anchors("plot") -}) - -/// Add an anchor to a plot environment -/// -/// This function is similar to `draw.anchor` but it takes an additional -/// axis tuple to specify which axis coordinate system to use. -/// -/// #example(``` -/// import cetz.plot -/// import cetz.draw: * -/// plot.plot(size: (2,2), name: "plot", -/// x-tick-step: none, y-tick-step: none, { -/// plot.add(((0,0), (1,1), (2,.5), (4,3))) -/// plot.add-anchor("pt", (1,1)) -/// }) -/// -/// line("plot.pt", ((), "|-", (0,1.5)), mark: (start: ">"), name: "line") -/// content("line.end", [Here], anchor: "south", padding: .1) -/// ```) -/// -/// - name (string): Anchor name -/// - position (tuple): Tuple of x and y values. -/// Both values can have the special values "min" and -/// "max", which resolve to the axis min/max value. -/// Position is in axis space defined by the axes passed to `axes`. -/// - axes (tuple): Name of the axes to use `("x", "y")` as coordinate -/// system for `position`. Note that both axes must be used, -/// as `add-anchors` does not create them on demand. -#let add-anchor(name, position, axes: ("x", "y")) = { - (( - type: "anchor", - name: name, - position: position, - axes: axes, - ),) -} diff --git a/src/lib/plot/annotation.typ b/src/lib/plot/annotation.typ deleted file mode 100644 index 1dcddb06f..000000000 --- a/src/lib/plot/annotation.typ +++ /dev/null @@ -1,78 +0,0 @@ -#import "util.typ" -#import "sample.typ" -#import "/src/draw.typ" -#import "/src/process.typ" -#import "/src/util.typ" -#import "/src/matrix.typ" - -/// Add an annotation to the plot -/// -/// An annotation is a sub-canvas that uses the plots coordinates specified -/// by its x and y axis. -/// -/// #example(``` -/// import cetz.plot -/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// plot.add(domain: (0, 2*calc.pi), calc.sin) -/// plot.annotate({ -/// rect((0, -1), (calc.pi, 1), fill: rgb(50,50,200,50)) -/// content((calc.pi, 0), [Here]) -/// }) -/// }) -/// ```) -/// -/// Bounds calculation is done naively, therefore fixed size content _can_ grow -/// out of the plot. You can adjust the padding manually to adjust for that. The -/// feature of solving the correct bounds for fixed size elements might be added -/// in the future. -/// -/// - body (drawable): Elements to draw -/// - axes (axes): X and Y axis names -/// - resize (bool): If true, the plots axes get adjusted to contain the annotation -/// - padding (none,number,dictionary): Annotation padding that is used for axis -/// adjustment -/// - background (bool): If true, the annotation is drawn behind all plots, in the background. -/// If false, the annotation is drawn above all plots. -#let annotate(body, axes: ("x", "y"), resize: true, padding: none, background: false) = { - (( - type: "annotation", - body: { - draw.set-style(mark: (transform-shape: false)) - body; - }, - axes: axes, - resize: resize, - background: background, - padding: util.as-padding-dict(padding), - ),) -} - -// Returns the adjusted axes for the annotation object -// -// -> array Tuple of x and y axis -#let calc-annotation-domain(ctx, x, y, annotation) = { - if not annotation.resize { - return (x, y) - } - - ctx.transform = matrix.ident() - let (ctx: ctx, bounds: bounds, drawables: _) = process.many(ctx, annotation.body) - if bounds == none { - return (x, y) - } - - let (x-min, y-min, ..) = bounds.low - let (x-max, y-max, ..) = bounds.high - - x-min -= annotation.padding.left - x-max += annotation.padding.right - y-min -= annotation.padding.bottom - y-max += annotation.padding.top - - x.min = calc.min(x.min, x-min) - x.max = calc.max(x.max, x-max) - y.min = calc.min(y.min, y-min) - y.max = calc.max(y.max, y-max) - - return (x, y) -} diff --git a/src/lib/plot/bar.typ b/src/lib/plot/bar.typ deleted file mode 100644 index 60f228eca..000000000 --- a/src/lib/plot/bar.typ +++ /dev/null @@ -1,265 +0,0 @@ -#import "/src/draw.typ" -#import "/src/util.typ" - -#import "errorbar.typ": draw-errorbar - -#let _transform-row(row, x-key, y-key, error-key) = { - let x = row.at(x-key) - let y = if y-key == auto { - row.slice(1) - } else if type(y-key) == array { - y-key.map(k => row.at(k, default: 0)) - } else { - row.at(y-key, default: 0) - } - let err = if error-key == none { - 0 - } else if type(error-key) == array { - error-key.map(k => row.at(k, default: 0)) - } else { - row.at(error-key, default: 0) - } - - if type(y) != array { y = (y,) } - if type(err) != array { err = (err,) } - - (x, y.flatten(), err.flatten()) -} - -// Get a single items min and maximum y-value -#let _minmax-value(row) = { - let min = none - let max = none - - let y = row.at(1) - let e = row.at(2) - for i in range(0, y.len()) { - let i-min = y.at(i) - e.at(i, default: 0) - if min == none { min = i-min } - else { min = calc.min(min, i-min) } - - let i-max = y.at(i) + e.at(i, default: 0) - if max == none { max = i-max } - else { max = calc.max(max, i-max) } - } - - return (min: min, max: max) -} - -// Functions for max value calculation -#let _max-value-fn = ( - basic: (data, min: 0) => { - calc.max(min, ..data.map(t => _minmax-value(t).max)) - }, - clustered: (data, min: 0) => { - calc.max(min, ..data.map(t => _minmax-value(t).max)) - }, - stacked: (data, min: 0) => { - calc.max(min, ..data.map(t => t.at(1).sum())) - }, - stacked100: (.., min: 0) => {min + 100} -) - -// Functions for min value calculation -#let _min-value-fn = ( - basic: (data, min: 0) => { - calc.min(min, ..data.map(t => _minmax-value(t).min)) - }, - clustered: (data, min: 0) => { - calc.min(min, ..data.map(t => _minmax-value(t).min)) - }, - stacked: (data, min: 0) => { - calc.min(min, ..data.map(t => t.at(1).sum())) - }, - stacked100: (.., min: 0) => {min} -) - -#let _prepare(self, ctx) = { - return self -} - -#let _get-x-offset(position, width) = { - if position == "start" { 0 } - else if position == "end" { width } - else { width / 2 } -} - -#let _draw-rects(filling, self, ctx, ..args) = { - let x-axis = ctx.x - let y-axis = ctx.y - - let bars = () - let errors = () - - let w = self.bar-width - for d in self.data { - let (x, n, len, y-min, y-max, err) = d - - let w = self.bar-width - let gap = self.cluster-gap * if w > 0 { -1 } else { +1 } - w += gap * (len - 1) - - let x-offset = _get-x-offset(self.bar-position, self.bar-width) - x-offset += gap * n - - let left = x - x-offset - let right = left + w - let width = (right - left) / len - - if self.mode in ("basic", "clustered") { - left = left + width * n - right = left + width - } - - if (left <= x-axis.max and right >= x-axis.min and - y-min <= y-axis.max and y-max >= y-axis.min) { - left = calc.max(left, x-axis.min) - right = calc.min(right, x-axis.max) - y-min = calc.max(y-min, y-axis.min) - y-max = calc.min(y-max, y-axis.max) - - draw.rect((left, y-min), (right, y-max)) - - if not filling and err != 0 { - let y-whisker-size = self.whisker-size * ctx.x-scale - draw-errorbar(((left + right) / 2, y-max), - 0, err, 0, y-whisker-size / 2, self.style + self.error-style) - } - } - } -} - -#let _stroke(self, ctx) = { - _draw-rects(false, self, ctx, fill: none) -} - -#let _fill(self, ctx) = { - _draw-rects(true, self, ctx, stroke: none) -} - -/// Add a bar- or column-chart to the plot -/// -/// A bar- or column-chart is a chart where values are drawn as rectangular boxes. -/// -/// - data (array): Array of data items. An item is an array containing a x an one or more y values. -/// For example `(0, 1)` or `(0, 10, 5, 30)`. Depending on the `mode`, the data items -/// get drawn as either clustered or stacked rects. -/// - x-key: (int,string): Key to use for retreiving a bars x-value from a single data entry. -/// This value gets passed to the `.at(...)` function of a data item. -/// - y-key: (auto,int,string,array): Key to use for retreiving a bars y-value. For clustered/stacked -/// data, this must be set to a list of keys (e.g. `range(1, 4)`). If set to `auto`, att but the first -/// array-values of a data item are used as y-values. -/// - error-key: (none,int,string): Key to use for retreiving a bars y-error. -/// - mode (string): The mode on how to group data items into bars: -/// / basic: Add one bar per data value. If the data contains multiple values, -/// group those bars next to each other. -/// / clustered: Like "basic", but take into account the maximum number of values of all items -/// and group each cluster of bars together having the width of the widest cluster. -/// / stacked: Stack bars of subsequent item values onto the previous bar, generating bars -/// with the height of the sume of all an items values. -/// / stacked100: Like "stacked", but scale each bar to height $100$, making the different -/// bars percentages of the sum of an items values. -/// - labels (none,content,array): A single legend label for "basic" bar-charts, or a -/// a list of legend labels per bar category, if the mode is one of "clustered", "stacked" or "stacked100". -/// - bar-width (float): Width of one data item on the y axis -/// - bar-position (string): Positioning of data items relative to their x value. -/// - "start": The lower edge of the data item is on the x value (left aligned) -/// - "center": The data item is centered on the x value -/// - "end": The upper edge of the data item is on the x value (right aligned) -/// - cluster-gap (float): Spacing between bars insides a cluster. -/// - style (dictionary): Plot style -/// - axes (axes): Plot axes. To draw a horizontal growing bar chart, you can swap the x and y axes. -#let add-bar(data, - x-key: 0, - y-key: auto, - error-key: none, - mode: "basic", - labels: none, - bar-width: 1, - bar-position: "center", - cluster-gap: 0, - whisker-size: .25, - error-style: (:), - style: (:), - axes: ("x", "y")) = { - assert(mode in ("basic", "clustered", "stacked", "stacked100"), - message: "Mode must be basic, clustered, stacked or stacked100, but is " + mode) - assert(bar-position in ("start", "center", "end"), - message: "Invalid bar-position '" + bar-position + "'. Allowed values are: start, center, end") - assert(bar-width != 0, - message: "Option bar-width must be != 0, but is " + str(bar-width)) - if error-key != none { - assert(y-key != auto, - message: "Bar value-key must be set != auto if error-key is set") - assert(mode in ("basic", "clustered"), - message: "Error bars are supported for basic or clustered only, got " + mode) - } - - // Transform data to (x, y, error) triplets - let data = data.map(row => _transform-row(row, x-key, y-key, error-key)) - - let n = util.max(..data.map(d => d.at(1).len())) - let x-offset = _get-x-offset(bar-position, bar-width) - let x-domain = (util.min(..data.map(d => d.at(0))) - x-offset, - util.max(..data.map(d => d.at(0))) - x-offset + bar-width) - let y-domain = (_min-value-fn.at(mode)(data), - _max-value-fn.at(mode)(data)) - - // For stacked 100%, multiply each column/bar - if mode == "stacked100" { - data = data.map(((x, y, err)) => { - let f = 100 / y.sum() - return (x, y.map(v => v * f), err) - }) - } - - // Transform data from (x, ..y) to (x, n, len, y-min, y-max) per y - let stacked = mode in ("stacked", "stacked100") - let clustered = mode == "clustered" - let bar-data = if mode == "basic" { - range(0, data.len()).map(_ => ()) - } else { - range(0, n).map(_ => ()) - } - - let j = 0 - for (x, y, err) in data { - let len = if clustered { n } else { y.len() } - let sum = 0 - for (i, y) in y.enumerate() { - let err = err.at(i, default: 0) - if stacked { - bar-data.at(i).push((x, i, len, sum, sum + y, err)) - } else if clustered { - bar-data.at(i).push((x, i, len, 0, y, err)) - } else { - bar-data.at(j).push((x, i, len, 0, y, err)) - } - sum += y - } - j += 1 - } - - let labels = if type(labels) == array { labels } else { (labels,) } - range(0, bar-data.len()).map(i => ( - type: "bar", - label: labels.at(i, default: none), - axes: axes, - mode: mode, - data: bar-data.at(i), - x-domain: x-domain, - y-domain: y-domain, - style: style, - bar-width: bar-width, - bar-position: bar-position, - cluster-gap: cluster-gap, - whisker-size: whisker-size, - error-style: error-style, - plot-prepare: _prepare, - plot-stroke: _stroke, - plot-fill: _fill, - plot-legend-preview: self => { - draw.rect((0,0), (1,1), ..self.style) - } - )) -} diff --git a/src/lib/plot/boxwhisker.typ b/src/lib/plot/boxwhisker.typ deleted file mode 100644 index 6d7d87b51..000000000 --- a/src/lib/plot/boxwhisker.typ +++ /dev/null @@ -1,118 +0,0 @@ -#import "/src/draw.typ" -#import "/src/util.typ" - -/// Add one or more box or whisker plots -/// -/// #example(``` -/// cetz.plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// cetz.plot.add-boxwhisker((x: 1, // Location on x-axis -/// outliers: (7, 65, 69), // Optional outlier values -/// min: 15, max: 60, // Minimum and maximum -/// q1: 25, // Quartiles: Lower -/// q2: 35, // Median -/// q3: 50)) // Upper -/// }) -/// ```) -/// -/// - data (array, dictionary): dictionary or array of dictionaries containing the -/// needed entries to plot box and whisker plot. -/// -/// The following fields are supported: -/// - `x` (number) X-axis value -/// - `min` (number) Minimum value -/// - `max` (number) Maximum value -/// - `q1`, `q2`, `q3` (number) Quartiles from lower to to upper -/// - `outliers` (array of number) Optional outliers -/// -/// - 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 -/// - label (none,content): Legend label to show for this plot. -#let add-boxwhisker(data, - label: none, - 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 '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") - - // 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 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", - label: label, - 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: (:) - ) }),) -} diff --git a/src/lib/plot/contour.typ b/src/lib/plot/contour.typ deleted file mode 100644 index e75998eff..000000000 --- a/src/lib/plot/contour.typ +++ /dev/null @@ -1,349 +0,0 @@ -#import "util.typ" -#import "sample.typ" -#import "/src/draw.typ" - -// Find contours of a 2D array by using marching squares algorithm -// -// - data (array): A 2D array of floats where the first index is the row and the second index is the column -// - offset (float): Z value threshold of a cell compare with `op` to, to count as true -// - op (auto,string,function): Z value comparison oparator: -// / `">", ">=", "<", "<=", "!=", "=="`: Use the passed operator to compare z. -// / `auto`: Use ">=" for positive z values, "<=" for negative z values. -// / ``: If set to a function, that function gets called -// with two arguments, the z value `z1` to compare against and -// the z value `z2` of the data and must return a boolean: `(z1, z2) => boolean`. -// - interpolate (bool): Enable cell interpolation for smoother lines -// - contour-limit (int): Contour limit after which the algorithm panics -// -> array: Array of contour point arrays -#let find-contours(data, offset, op: auto, interpolate: true, contour-limit: 50) = { - assert(data != none and type(data) == array, - message: "Data must be of type array") - assert(type(offset) in (int, float), - message: "Offset must be numeric") - - let n-rows = data.len() - let n-cols = data.at(0).len() - if n-rows < 2 or n-cols < 2 { - return () - } - - assert(op == auto or type(op) in (str, function), - message: "Operator must be of type auto, string or function") - if op == auto { - op = if offset < 0 { "<=" } else { ">=" } - } - if type(op) == str { - assert(op in ("<", "<=", ">", ">=", "==", "!="), - message: "Operator must be one of: <, <=, >, >=, != or ==") - } - - // Return if data is set - let is-set = if type(op) == function { - v => op(offset, v) - } else if op == "==" { - v => v == offset - } else if op == "!=" { - v => v != offset - } else if op == "<" { - v => v < offset - } else if op == "<=" { - v => v <= offset - } else if op == ">" { - v => v > offset - } else if op == ">=" { - v => v >= offset - } - - // Build a binary map that has 0 for unset and 1 for set cells - let bin-data = data.map(r => r.map(is-set)) - - // Get binary data at x, y - let get-bin(x, y) = { - if x >= 0 and x < n-cols and y >= 0 and y < n-rows { - return bin-data.at(y).at(x) - } - return false - } - - // Get data point for x, y coordinate - let get-data(x, y) = { - if x >= 0 and x < n-cols and y >= 0 and y < n-rows { - return float(data.at(y).at(x)) - } - return none - } - - // Get case (0 to 15) - let get-case(tl, tr, bl, br) = { - int(tl) * 8 + int(tr) * 4 + int(br) * 2 + int(bl) - } - - let lerp(a, b) = { - if a == b { return a } - else if a == none { return 1 } - else if b == none { return 0 } - return (offset - a) / (b - a) - } - - // List of all found contours - let contours = () - - let segments = () - for y in range(-1, n-rows) { - for x in range(-1, n-cols) { - let tl = get-bin(x, y) - let tr = get-bin(x+1, y) - let bl = get-bin(x, y+1) - let br = get-bin(x+1, y+1) - - // Corner data - // - // nw-----ne - // | | - // | | - // | | - // sw-----se - let nw = get-data(x, y) - let ne = get-data(x+1, y) - let se = get-data(x+1, y+1) - let sw = get-data(x, y+1) - - // Interpolated edge points - // - // +-- a --+ - // | | - // d b - // | | - // +-- c --+ - let a = (x + .5, y) - let b = (x + 1, y + .5) - let c = (x + .5, y + 1) - let d = (x, y + .5) - if interpolate { - a = (x + lerp(nw, ne), y) - b = (x + 1, y + lerp(ne, se)) - c = (x + lerp(sw, se), y + 1) - d = (x, y + lerp(nw, sw)) - } - - let case = get-case(tl, tr, bl, br) - if case in (1, 14) { - segments.push((d, c)) - } else if case in (2, 13) { - segments.push((b, c)) - } else if case in (3, 12) { - segments.push((d, b)) - } else if case in (4, 11) { - segments.push((a, b)) - } else if case == 5 { - segments.push((d, a)) - segments.push((c, b)) - } else if case in (6, 9) { - segments.push((c, a)) - } else if case in (7, 8) { - segments.push((d, a)) - } else if case == 10 { - segments.push((a, b)) - segments.push((c, d)) - } - } - } - - // Join lines to one or more contours - // This is done by searching for the next line - // that starts at the current contours head or tail - // point. If found, push the other coordinate to - // the contour. If no line could be found, push a - // new contour. - let contours = () - while segments.len() > 0 { - if contours.len() == 0 { - contours.push(segments.remove(0)) - } - - let found = false - - let i = 0 - while i < segments.len() { - let (a, b) = segments.at(i) - let (h, t) = (contours.last().first(), - contours.last().last()) - if a == t { - contours.last().push(b) - segments.remove(i) - found = true - } else if b == t { - contours.last().push(a) - segments.remove(i) - found = true - } else if a == h { - contours.last().insert(0, b) - segments.remove(i) - found = true - } else if b == h { - contours.last().insert(0, a) - segments.remove(i) - found = true - } else { - i += 1 - } - } - - // Insert the next contour - if not found { - contours.push(segments.remove(0)) - } - - // Check limit - assert(contours.len() <= contour-limit, - message: "Countour limit reached! Raise contour-limit if you " + - "think this is not an error") - } - - return contours -} - -// Prepare line data -#let _prepare(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - - self.contours = self.contours.map(c => { - c.stroke-paths = util.compute-stroke-paths(c.line-data, - (x.min, y.min), (x.max, y.max)) - - if self.fill { - c.fill-paths = util.compute-fill-paths(c.line-data, - (x.min, y.min), (x.max, y.max)) - } - return c - }) - - return self -} - -// Stroke line data -#let _stroke(self, ctx) = { - for c in self.contours { - for p in c.stroke-paths { - draw.line(..p, fill: none, close: p.first() == p.last()) - } - } -} - -// Fill line data -#let _fill(self, ctx) = { - if not self.fill { return } - for c in self.contours { - for p in c.fill-paths { - draw.line(..p, stroke: none, close: p.first() == p.last()) - } - } -} - -/// Add a contour plot of a sampled function or a matrix. -/// -/// #example(``` -/// cetz.plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// cetz.plot.add-contour(x-domain: (-3, 3), y-domain: (-3, 3), -/// style: (fill: rgb(50,50,250,50)), -/// fill: true, -/// op: "<", // Find contours where data < z -/// z: (2.5, 2, 1), // Z values to find contours for -/// (x, y) => calc.sqrt(x * x + y * y)) -/// }) -/// ```) -/// -/// - data (array, function): A function of the signature `(x, y) => z` -/// or an array of arrays of floats (a matrix) where the first -/// index is the row and the second index is the column. -/// - z (float, array): Z values to plot. Contours containing values -/// above z (z >= 0) or below z (z < 0) get plotted. -/// If you specify multiple z values, they get plotted in the order of specification. -/// - x-domain (domain): X axis domain used if `data` is a function, that is the -/// domain inside the function gets sampled. -/// - y-domain (domain): Y axis domain used if `data` is a function, see `x-domain`. -/// - x-samples (int): X axis domain samples (2 < n). Note that contour finding -/// can be quite slow. Using a big sample count can improve accuracy but can -/// also lead to bad compilation performance. -/// - y-samples (int): Y axis domain samples (2 < n) -/// - interpolate (bool): Use linear interpolation between sample values which can -/// improve the resulting plot, especially if the contours are curved. -/// - op (auto,string,function): Z value comparison oparator: -/// / `">", ">=", "<", "<=", "!=", "=="`: Use the operator for comparison of `z` to -/// the values from `data`. -/// / `auto`: Use ">=" for positive z values, "<=" for negative z values. -/// / ``: Call comparison function of the format `(plot-z, data-z) => boolean`, -/// where `plot-z` is the z-value from the plots `z` argument and `data-z` -/// is the z-value of the data getting plotted. The function must return true -/// if at the combinations of arguments a contour is detected. -/// - fill (bool): Fill each contour -/// - style (style): Style to use for plotting, can be used with a palette function. Note -/// that all z-levels use the same style! -/// - axes (axes): Name of the axes to use for plotting. -/// - limit (int): Limit of contours to create per z value before the function panics -/// - label (none,content): Plot legend label to show. The legend preview for -/// contour plots is a little rectangle drawn with the contours style. -#let add-contour(data, - label: none, - z: (1,), - x-domain: (0, 1), - y-domain: (0, 1), - x-samples: 25, - y-samples: 25, - interpolate: true, - op: auto, - axes: ("x", "y"), - style: (:), - fill: false, - limit: 50, - ) = { - // Sample a x/y function - if type(data) == function { - data = sample.sample-fn2(data, - x-domain, y-domain, - x-samples, y-samples) - } - - // Find matrix dimensions - assert(type(data) == array) - let (x-min, x-max) = x-domain - let dx = (x-max - x-min) / (data.at(0).len() - 1) - let (y-min, y-max) = y-domain - let dy = (y-max - y-min) / (data.len() - 1) - - let contours = () - let z = if type(z) == array { z } else { (z,) } - for z in z { - for contour in find-contours(data, z, op: op, interpolate: interpolate, contour-limit: limit) { - let line-data = contour.map(pt => { - (pt.at(0) * dx + x-min, - pt.at(1) * dy + y-min) - }) - - contours.push(( - z: z, - line-data: line-data, - )) - } - } - - return (( - type: "contour", - label: label, - contours: contours, - axes: axes, - x-domain: x-domain, - y-domain: y-domain, - style: style, - fill: fill, - mark: none, - mark-style: none, - plot-prepare: _prepare, - plot-stroke: _stroke, - plot-fill: _fill, - plot-legend-preview: self => { - if not self.fill { self.style.fill = none } - draw.rect((0,0), (1,1), ..self.style) - } - ),) -} diff --git a/src/lib/plot/errorbar.typ b/src/lib/plot/errorbar.typ deleted file mode 100644 index 885fef1ce..000000000 --- a/src/lib/plot/errorbar.typ +++ /dev/null @@ -1,120 +0,0 @@ -#import "/src/draw.typ" -#import "/src/util.typ" -#import "/src/vector.typ" - -#let _draw-whisker(pt, dir, ..style) = { - let a = vector.add(pt, vector.scale(dir, -1)) - let b = vector.add(pt, vector.scale(dir, +1)) - - draw.line(a, b, ..style) -} - -#let draw-errorbar(pt, x, y, x-whisker-size, y-whisker-size, style) = { - if type(x) != array { x = (-x, x) } - if type(y) != array { y = (-y, y) } - - let (x-min, x-max) = x - let x-min-pt = vector.add(pt, (x-min, 0)) - let x-max-pt = vector.add(pt, (x-max, 0)) - if x-min != 0 or x-max != 0 { - draw.line(x-min-pt, x-max-pt, ..style) - if x-whisker-size > 0 { - if x-min != 0 { - _draw-whisker(x-min-pt, (0, x-whisker-size), ..style) - } - if x-max != 0 { - _draw-whisker(x-max-pt, (0, x-whisker-size), ..style) - } - } - } - - let (y-min, y-max) = y - let y-min-pt = vector.add(pt, (0, y-min)) - let y-max-pt = vector.add(pt, (0, y-max)) - if y-min != 0 or y-max != 0 { - draw.line(y-min-pt, y-max-pt, ..style) - if y-whisker-size > 0 { - if y-min != 0 { - _draw-whisker(y-min-pt, (y-whisker-size, 0), ..style) - } - if y-max != 0 { - _draw-whisker(y-max-pt, (y-whisker-size, 0), ..style) - } - } - } -} - -#let _prepare(self, ctx) = { - return self -} - -#let _stroke(self, ctx) = { - let x-whisker-size = self.whisker-size * ctx.y-scale - let y-whisker-size = self.whisker-size * ctx.x-scale - - draw-errorbar((self.x, self.y), - self.x-error, self.y-error, - x-whisker-size, y-whisker-size, - self.style) -} - -/// Add x- and/or y-error bars -/// -/// - pt (tuple): Error-bar center coordinate tuple: `(x, y)` -/// - x-error: (float,tuple): Single error or tuple of errors along the x-axis -/// - y-error: (float,tuple): Single error or tuple of errors along the y-axis -/// - mark: (none,string): Mark symbol to show at the error position (`pt`). -/// - mark-size: (number): Size of the mark symbol. -/// - mark-style: (style): Extra style to apply to the mark symbol. -/// - whisker-size (float): Width of the error bar whiskers in canvas units. -/// - style (dictionary): Style for the error bars -/// - label: (none,content): Label to tsh -/// - axes (axes): Plot axes. To draw a horizontal growing bar chart, you can swap the x and y axes. -#let add-errorbar(pt, - x-error: 0, - y-error: 0, - label: none, - mark: "o", - mark-size: .2, - mark-style: (:), - whisker-size: .5, - style: (:), - axes: ("x", "y")) = { - assert(x-error != 0 or y-error != 0, - message: "Either x-error or y-error must be set.") - - let (x, y) = pt - - if type(x-error) != array { - x-error = (x-error, x-error) - } - if type(y-error) != array { - y-error = (y-error, y-error) - } - - x-error.at(0) = calc.abs(x-error.at(0)) * -1 - y-error.at(0) = calc.abs(y-error.at(0)) * -1 - - let x-domain = x-error.map(v => v + x) - let y-domain = y-error.map(v => v + y) - - return (( - type: "errorbar", - label: label, - axes: axes, - data: ((x,y),), - x: x, - y: y, - x-error: x-error, - y-error: y-error, - x-domain: x-domain, - y-domain: y-domain, - mark: mark, - mark-size: mark-size, - mark-style: mark-style, - whisker-size: whisker-size, - style: style, - plot-prepare: _prepare, - plot-stroke: _stroke, - ),) -} diff --git a/src/lib/plot/legend.typ b/src/lib/plot/legend.typ deleted file mode 100644 index e0b95ab6c..000000000 --- a/src/lib/plot/legend.typ +++ /dev/null @@ -1,205 +0,0 @@ -#import "/src/draw.typ" -#import "/src/anchor.typ" as anchor_ -#import "/src/styles.typ" -#import "mark.typ": draw-mark-shape -#import draw: group - -#let default-style = ( - orientation: ttb, - default-position: "north-east", - layer: 1, // Legend layer - fill: rgb(255,255,255,200), // Legend background - stroke: black, // Legend border - padding: .1, // Legend border padding - offset: (0, 0), // Legend displacement - spacing: .1, // Spacing between anchor and legend - item: ( - radius: 0, - spacing: .05, // Spacing between items - preview: ( - width: .75, // Preview width - height: .3, // Preview height - margin: .1 // Distance between preview and label - ) - ), - radius: 0, -) - -// Map position to legend group anchor -#let auto-group-anchor = ( - inner-north-west: "north-west", - inner-north: "north", - inner-north-east: "north-east", - inner-south-west: "south-west", - inner-south: "south", - inner-south-east: "south-east", - inner-west: "west", - inner-east: "east", - north-west: "north-east", - north: "south", - north-east: "north-west", - south-west: "south-east", - south: "north", - south-east: "south-west", - east: "west", - west: "east", -) - -// Generate legend positioning anchors -#let add-legend-anchors(style, element, size) = { - import draw: * - let (w, h) = size - let (xo, yo) = { - let spacing = style.at("spacing", default: (0, 0)) - if type(spacing) == array { - spacing - } else { - (spacing, spacing) - } - } - - anchor("north", (rel: (w / 2, yo), to: (element + ".north", "-|", element + ".origin"))) - anchor("south", (rel: (w / 2, -yo), to: (element + ".south", "-|", element + ".origin"))) - anchor("east", (rel: (xo, h / 2), to: (element + ".east", "|-", element + ".origin"))) - anchor("west", (rel: (-xo, h / 2), to: (element + ".west", "|-", element + ".origin"))) - anchor("north-east", (rel: (xo, h), to: (element + ".north-east", "|-", element + ".origin"))) - anchor("north-west", (rel: (-xo, h), to: (element + ".north-west", "|-", element + ".origin"))) - anchor("south-east", (rel: (xo, 0), to: (element + ".south-east", "|-", element + ".origin"))) - anchor("south-west", (rel: (-xo, 0), to: (element + ".south-west", "|-", element + ".origin"))) - anchor("inner-north", (rel: (w / 2, h - yo), to: element + ".origin")) - anchor("inner-north-east", (rel: (w - xo, h - yo), to: element + ".origin")) - anchor("inner-north-west", (rel: (yo, h - yo), to: element + ".origin")) - anchor("inner-south", (rel: (w / 2, yo), to: element + ".origin")) - anchor("inner-south-east", (rel: (w - xo, yo), to: element + ".origin")) - anchor("inner-south-west", (rel: (xo, yo), to: element + ".origin")) - anchor("inner-east", (rel: (w - xo, h / 2), to: element + ".origin")) - anchor("inner-west", (rel: (xo, h / 2), to: element + ".origin")) -} - -// Draw a generic item preview -#let draw-generic-preview(item) = { - import draw: * - - if item.at("fill", default: false) { - rect((0,0), (1,1), ..item.style) - } else { - line((0,.5), (1,.5), ..item.style) - } -} - -/// Construct a legend item for use with the `legend` function -/// -/// - label (none, auto, content): Legend label or auto to use the enumerated default label -/// - preview (auto, function): Legend preview icon function of the format `item => elements`. -/// Note that the canvas bounds for drawing the preview are (0,0) to (1,1). -/// - mark: (none,string): Legend mark symbol -/// - mark-style: (none,dictionary): Mark style -/// - mark-size: (number): Mark size -/// - style (styles): Style keys for the single item -#let item(label, preview, mark: none, mark-style: (:), mark-size: 1, ..style) = { - assert.eq(style.pos().len(), 0, - message: "Unexpected positional arguments") - return ((label: label, preview: preview, - mark: mark, mark-style: mark-style, mark-size: mark-size, - style: style.named()),) -} - -/// Draw a legend -#let legend(position, items, name: "legend", ..style) = group(name: name, ctx => { - draw.anchor("default", ()) - let items = if items != none { items.filter(v => v.label != none) } else { () } - if items == () { - return - } - - let style = styles.resolve( - ctx.style, merge: style.named(), base: default-style, root: "legend") - assert(style.orientation in (ttb, ltr), - message: "Unsupported legend orientation.") - - // Position - let position = if position == auto { - style.default-position - } else { - position - } - - // Adjust anchor - if style.anchor == auto { - style.anchor = if type(position) == str { - auto-group-anchor.at(position, default: "north-west") - } else { - "north-west" - } - } - - // Apply offset - if style.offset not in (none, (0,0)) { - position = (rel: style.offset, to: position) - } - - // Draw items - draw.on-layer(style.layer, { - draw.group(name: "items", padding: style.padding, ctx => { - import draw: * - - set-origin(position) - anchor("default", (0,0)) - - let pt = (0, 0) - for (i, item) in items.enumerate() { - let (label, preview) = item - if label == none { - continue - } else if label == auto { - label = $ f_(#i) $ - } - - group({ - anchor("default", (0,0)) - - let row-height = style.item.preview.height - let preview-width = style.item.preview.width - let preview-a = (0, -row-height / 2) - let preview-b = (preview-width, +row-height / 2) - let label-west = (preview-width + style.item.preview.margin, 0) - - // Draw item preview - let draw-preview = if preview == auto { draw-generic-preview } else { preview } - group({ - set-viewport(preview-a, preview-b, bounds: (1, 1, 0)) - (draw-preview)(item) - }) - - // Draw mark preview - let mark = item.at("mark", default: none) - if mark != none { - draw-mark-shape((preview-a, 50%, preview-b), - calc.min(style.item.preview.width / 2, item.mark-size), - mark, - item.mark-style) - } - - // Draw label - content(label-west, - align(left + horizon, label), - name: "label", anchor: "west") - }, name: "item", anchor: if style.orientation == ltr { "west" } else { "north-west" }) - - if style.orientation == ttb { - set-origin((rel: (0, -style.item.spacing), - to: "item.south-west")) - } else if style.orientation == ltr { - set-origin((rel: (style.item.spacing, 0), - to: "item.east")) - } - } - }, anchor: style.anchor) - }) - - // Fill legend background - draw.on-layer(style.layer - .5, { - draw.rect("items.south-west", - "items.north-east", fill: style.fill, stroke: style.stroke, radius: style.radius) - }) -}) diff --git a/src/lib/plot/line.typ b/src/lib/plot/line.typ deleted file mode 100644 index be45f2d26..000000000 --- a/src/lib/plot/line.typ +++ /dev/null @@ -1,507 +0,0 @@ -#import "util.typ" -#import "sample.typ" -#import "/src/draw.typ" - -// Transform points -// -// - data (array): Data points -// - line (str,dictionary): Line line -#let transform-lines(data, line) = { - let hvh-data(t) = { - if type(t) == ratio { - t = t / 1% - } - t = calc.max(0, calc.min(t, 1)) - - let pts = () - - let len = data.len() - for i in range(0, len) { - pts.push(data.at(i)) - - if i < len - 1 { - let (a, b) = (data.at(i), data.at(i+1)) - if t == 0 { - pts.push((a.at(0), b.at(1))) - } else if t == 1 { - pts.push((b.at(0), a.at(1))) - } else { - let x = a.at(0) + (b.at(0) - a.at(0)) * t - pts.push((x, a.at(1))) - pts.push((x, b.at(1))) - } - } - } - return pts - } - - if type(line) == str { - line = (type: line) - } - - let line-type = line.at("type", default: "linear") - assert(line-type in ("raw", "linear", "spline", "vh", "hv", "hvh")) - - // Transform data into line-data - let line-data = if line-type == "linear" { - return util.linearized-data(data, line.at("epsilon", default: 0)) - } else if line-type == "spline" { - return util.sampled-spline-data(data, - line.at("tension", default: .5), - line.at("samples", default: 15)) - } else if line-type == "vh" { - return hvh-data(0) - } else if line-type == "hv" { - return hvh-data(1) - } else if line-type == "hvh" { - return hvh-data(line.at("mid", default: .5)) - } else { - return data - } -} - -// Fill a plot by generating a fill path to y value `to` -#let fill-segments-to(segments, to) = { - for s in segments { - let low = calc.min(..s.map(v => v.at(0))) - let high = calc.max(..s.map(v => v.at(0))) - - let origin = (low, to) - let target = (high, to) - - draw.line(origin, ..s, target, stroke: none) - } -} - -// Fill a shape by generating a fill path for each segment -#let fill-shape(paths) = { - for p in paths { - draw.line(..p, stroke: none) - } -} - -// Prepare line data -#let _prepare(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - - // Generate stroke paths - self.stroke-paths = util.compute-stroke-paths(self.line-data, - (x.min, y.min), (x.max, y.max)) - - // Compute fill paths if filling is requested - self.hypograph = self.at("hypograph", default: false) - self.epigraph = self.at("epigraph", default: false) - self.fill = self.at("fill", default: false) - if self.hypograph or self.epigraph or self.fill { - self.fill-paths = util.compute-fill-paths(self.line-data, - (x.min, y.min), (x.max, y.max)) - } - - return self -} - -// Stroke line data -#let _stroke(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - - for p in self.stroke-paths { - draw.line(..p, fill: none) - } -} - -// Fill line data -#let _fill(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - - if self.hypograph { - fill-segments-to(self.fill-paths, y.min) - } - if self.epigraph { - fill-segments-to(self.fill-paths, y.max) - } - if self.fill { - if self.at("fill-type", default: "axis") == "shape" { - fill-shape(self.fill-paths) - } else { - fill-segments-to(self.fill-paths, - calc.max(calc.min(y.max, 0), y.min)) - } - } -} - -/// Add data to a plot environment. -/// -/// Note: You can use this for scatter plots by setting -/// the stroke style to `none`: `add(..., style: (stroke: none))`. -/// -/// Must be called from the body of a `plot(..)` command. -/// -/// - domain (domain): Domain of `data`, if `data` is a function. Has no effect -/// if `data` is not a function. -/// - hypograph (bool): Fill hypograph; uses the `hypograph` style key for -/// drawing -/// - epigraph (bool): Fill epigraph; uses the `epigraph` style key for -/// drawing -/// - fill (bool): Fill the shape of the plot -/// - fill-type (string): Fill type: -/// / `"axis"`: Fill the shape to y = 0 -/// / `"shape"`: Fill the complete shape -/// - samples (int): Number of times the `data` function gets called for -/// sampling y-values. Only used if `data` is of type function. This parameter gets -/// passed onto `sample-fn`. -/// - sample-at (array): Array of x-values the function gets sampled at in addition -/// to the default sampling. This parameter gets passed to `sample-fn`. -/// - line (string, dictionary): Line type to use. The following types are -/// supported: -/// / `"linear"`: Draw linear lines between points -/// / `"spline"`: Calculate a Catmull-Rom through all points -/// / `"vh"`: Move vertical and then horizontal -/// / `"hv"`: Move horizontal and then vertical -/// / `"hvh"`: Add a vertical step in the middle -/// / `"raw"`: Like linear, but without linearization taking place. This is -/// meant as a "fallback" for either bad performance or bugs. -/// -/// If the value is a dictionary, the type must be -/// supplied via the `type` key. The following extra -/// attributes are supported: -/// / `"samples" `: Samples of splines -/// / `"tension" `: Tension of splines -/// / `"mid" `: Mid-Point of hvh lines (0 to 1) -/// / `"epsilon" `: Linearization slope epsilon for -/// use with `"linear"`, defaults to 0. -/// -/// #example(``` -/// import cetz.plot -/// let points(offset: 0) = ((0,0), (1,1), (2,0), (3,1), (4,0)).map(((x,y)) => { -/// (x,y + offset * 1.5) -/// }) -/// plot.plot(size: (12, 3), axis-style: none, { -/// plot.add(points(offset: 5), line: (type: "hvh", mid: .1)) -/// plot.add(points(offset: 4), line: "hvh") -/// plot.add(points(offset: 3), line: "hv") -/// plot.add(points(offset: 2), line: "vh") -/// plot.add(points(offset: 1), line: "spline") -/// plot.add(points(offset: 0), line: "linear") -/// }) -/// ```, vertical: true) -/// -/// - style (style): Style to use, can be used with a `palette` function -/// - axes (axes): Name of the axes to use for plotting. Reversing the axes -/// means rotating the plot by 90 degrees. -/// - mark (string): Mark symbol to place at each distinct value of the -/// graph. Uses the `mark` style key of `style` for drawing. -/// - mark-size (float): Mark size in cavas units -/// - data (array,function): Array of 2D data points (numeric) or a function -/// of the form `x => y`, where `x` is a value in `domain` -/// and `y` must be numeric or a 2D vector (for parametric functions). -/// #example(``` -/// import cetz.plot -/// plot.plot(size: (2, 2), axis-style: none, { -/// // Using an array of points: -/// plot.add(((0,0), (calc.pi/2,1), -/// (1.5*calc.pi,-1), (2*calc.pi,0))) -/// // Sampling a function: -/// plot.add(domain: (0, 2*calc.pi), calc.sin) -/// }) -/// ```) -/// - label (none,content): Legend label to show for this plot. -#let add(domain: auto, - hypograph: false, - epigraph: false, - fill: false, - fill-type: "axis", - style: (:), - mark: none, - mark-size: .2, - mark-style: (:), - samples: 50, - sample-at: (), - line: "linear", - axes: ("x", "y"), - label: none, - data - ) = { - // If data is of type function, sample it - if type(data) == function { - data = sample.sample-fn(data, domain, samples, sample-at: sample-at) - } - - // Transform data - let line-data = transform-lines(data, line) - - // Get x-domain - let x-domain = ( - calc.min(..line-data.map(t => t.at(0))), - calc.max(..line-data.map(t => t.at(0))) - ) - - // Get y-domain - let y-domain = if line-data != none {( - calc.min(..line-data.map(t => t.at(1))), - calc.max(..line-data.map(t => t.at(1))) - )} - - (( - type: "line", - label: label, - data: data, /* Raw data */ - line-data: line-data, /* Transformed data */ - axes: axes, - x-domain: x-domain, - y-domain: y-domain, - epigraph: epigraph, - hypograph: hypograph, - fill: fill, - fill-type: fill-type, - style: style, - mark: mark, - mark-size: mark-size, - mark-style: mark-style, - plot-prepare: _prepare, - plot-stroke: _stroke, - plot-fill: _fill, - plot-legend-preview: self => { - if self.fill or self.epigraph or self.hypograph { - draw.rect((0,0), (1,1), ..self.style) - } else { - draw.line((0,.5), (1,.5), ..self.style) - } - } - ),) -} - -/// Add horizontal lines at one or more y-values. Every lines start and end points -/// are at their axis bounds. -/// -/// #example(``` -/// cetz.plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// cetz.plot.add(domain: (0, 4*calc.pi), calc.sin) -/// // Add 3 horizontal lines -/// cetz.plot.add-hline(-.5, 0, .5) -/// }) -/// ```) -/// -/// - ..y (float): Y axis value(s) to add a line at -/// - min (auto,float): X axis minimum value or auto to take the axis minimum -/// - max (auto,float): X axis maximum value or auto to take the axis maximum -/// - axes (array): Name of the axes to use for plotting -/// - style (style): Style to use, can be used with a palette function -/// - label (none,content): Legend label to show for this plot. -#let add-hline(..y, - min: auto, - max: auto, - axes: ("x", "y"), - style: (:), - label: none, - ) = { - assert(y.pos().len() >= 1, - message: "Specify at least one y value") - assert(y.named().len() == 0) - - let prepare(self, ctx) = { - let (x-min, x-max) = (ctx.x.min, ctx.x.max) - let (y-min, y-max) = (ctx.y.min, ctx.y.max) - let x-min = if min == auto { x-min } else { min } - let x-max = if max == auto { x-max } else { max } - - self.lines = self.y.filter(y => y >= y-min and y <= y-max) - .map(y => ((x-min, y), (x-max, y))) - return self - } - - let stroke(self, ctx) = { - for (a, b) in self.lines { - draw.line(a, b, fill: none) - } - } - - let x-min = if min == auto { none } else { min } - let x-max = if max == auto { none } else { max } - - (( - type: "hline", - label: label, - y: y.pos(), - x-domain: (x-min, x-max), - y-domain: (calc.min(..y.pos()), calc.max(..y.pos())), - axes: axes, - style: style, - plot-prepare: prepare, - plot-stroke: stroke, - ),) -} - -/// Add vertical lines at one or more x-values. Every lines start and end points -/// are at their axis bounds. -/// -/// #example(``` -/// cetz.plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// cetz.plot.add(domain: (0, 2*calc.pi), calc.sin) -/// // Add 3 vertical lines -/// cetz.plot.add-vline(calc.pi/2, calc.pi, 3*calc.pi/2) -/// }) -/// ```) -/// -/// - ..x (float): X axis values to add a line at -/// - min (auto,float): Y axis minimum value or auto to take the axis minimum -/// - max (auto,float): Y axis maximum value or auto to take the axis maximum -/// - axes (array): Name of the axes to use for plotting, 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 -/// - label (none,content): Legend label to show for this plot. -#let add-vline(..x, - min: auto, - max: auto, - axes: ("x", "y"), - style: (:), - label: none, - ) = { - assert(x.pos().len() >= 1, - message: "Specify at least one x value") - assert(x.named().len() == 0) - - let prepare(self, ctx) = { - let (x-min, x-max) = (ctx.x.min, ctx.x.max) - let (y-min, y-max) = (ctx.y.min, ctx.y.max) - let y-min = if min == auto { y-min } else { min } - let y-max = if max == auto { y-max } else { max } - - self.lines = self.x.filter(x => x >= x-min and x <= x-max) - .map(x => ((x, y-min), (x, y-max))) - return self - } - - let stroke(self, ctx) = { - for (a, b) in self.lines { - draw.line(a, b, fill: none) - } - } - - let y-min = if min == auto { none } else { min } - let y-max = if max == auto { none } else { max } - - (( - type: "vline", - label: label, - x: x.pos(), - x-domain: (calc.min(..x.pos()), calc.max(..x.pos())), - y-domain: (y-min, y-max), - axes: axes, - style: style, - plot-prepare: prepare, - plot-stroke: stroke - ),) -} - -/// Fill the area between two graphs. This behaves same as `add` but takes -/// a pair of data instead of a single data array/function. -/// The area between both function plots gets filled. For a more detailed -/// explanation of the arguments, see @@add(). -/// -/// This can be used to display an error-band of a function. -/// -/// #example(``` -/// cetz.plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, { -/// cetz.plot.add-fill-between(domain: (0, 2*calc.pi), -/// calc.sin, // First function/data -/// calc.cos) // Second function/data -/// }) -/// ```) -/// -/// - domain (domain): Domain of both `data-a` and `data-b`. The domain is used for -/// sampling functions only and has no effect on data arrays. -/// - samples (int): Number of times the `data-a` and `data-b` function gets called for -/// sampling y-values. Only used if `data-a` or `data-b` is of -/// type function. -/// - sample-at (array): Array of x-values the function(s) get sampled at in addition -/// to the default sampling. -/// - line (string, dictionary): Line type to use, see @@add(). -/// - style (style): Style to use, can be used with a palette function. -/// - label (none,content): Legend label to show for this plot. -/// - axes (array): Name of the axes to use for plotting. -/// - data-a (array,function): Data of the first plot, see @@add(). -/// - data-b (array,function): Data of the second plot, see @@add(). -#let add-fill-between(data-a, - data-b, - domain: auto, - samples: 50, - sample-at: (), - line: "linear", - axes: ("x", "y"), - label: none, - style: (:)) = { - // If data is of type function, sample it - if type(data-a) == function { - data-a = sample.sample-fn(data-a, domain, samples, sample-at: sample-at) - } - if type(data-b) == function { - data-b = sample.sample-fn(data-b, domain, samples, sample-at: sample-at) - } - - // Transform data - let line-a-data = transform-lines(data-a, line) - let line-b-data = transform-lines(data-b, line) - - // Get x-domain - let x-domain = ( - calc.min(..line-a-data.map(t => t.at(0)), - ..line-b-data.map(t => t.at(0))), - calc.max(..line-a-data.map(t => t.at(0)), - ..line-b-data.map(t => t.at(0))) - ) - - // Get y-domain - let y-domain = if line-a-data != none and line-b-data != none {( - calc.min(..line-a-data.map(t => t.at(1)), - ..line-b-data.map(t => t.at(1))), - calc.max(..line-a-data.map(t => t.at(1)), - ..line-b-data.map(t => t.at(1))) - )} - - let prepare(self, ctx) = { - let (x, y) = (ctx.x, ctx.y) - - // Generate stroke paths - self.stroke-paths = ( - a: util.compute-stroke-paths(self.line-data.a, - (x.min, y.min), (x.max, y.max)), - b: util.compute-stroke-paths(self.line-data.b, - (x.min, y.min), (x.max, y.max)) - ) - - // Generate fill paths - self.fill-paths = util.compute-fill-paths(self.line-data.a + self.line-data.b.rev(), - (x.min, y.min), (x.max, y.max)) - - return self - } - - let stroke(self, ctx) = { - for p in self.stroke-paths.a { - draw.line(..p, fill: none) - } - for p in self.stroke-paths.b { - draw.line(..p, fill: none) - } - } - - let fill(self, ctx) = { - fill-shape(self.fill-paths) - } - - (( - type: "fill-between", - label: label, - axes: axes, - line-data: (a: line-a-data, b: line-b-data), - x-domain: x-domain, - y-domain: y-domain, - style: style, - plot-prepare: prepare, - plot-stroke: stroke, - plot-fill: fill, - plot-legend-preview: self => { - draw.rect((0,0), (1,1), ..self.style) - } - ),) -} diff --git a/src/lib/plot/mark.typ b/src/lib/plot/mark.typ deleted file mode 100644 index 8aae1f93e..000000000 --- a/src/lib/plot/mark.typ +++ /dev/null @@ -1,51 +0,0 @@ -#import "/src/draw.typ" - -// Draw mark at point with size -#let draw-mark-shape(pt, size, mark, style) = { - let (sx, sy) = if type(size) != array { - (size, size) - } else { size } - - let bl(pt) = (rel: (-sx/2, -sy/2), to: pt) - let br(pt) = (rel: (sx/2, -sy/2), to: pt) - let tl(pt) = (rel: (-sx/2, sy/2), to: pt) - let tr(pt) = (rel: (sx/2, sy/2), to: pt) - let ll(pt) = (rel: (-sx/2, 0), to: pt) - let rr(pt) = (rel: (sx/2, 0), to: pt) - let tt(pt) = (rel: (0, sy/2), to: pt) - let bb(pt) = (rel: (0, -sy/2), to: pt) - - if mark == "o" { - draw.circle(pt, radius: (sx/2, sy/2), ..style) - } else if mark == "square" { - draw.rect(bl(pt), tr(pt), ..style) - } else if mark == "triangle" { - draw.line(bl(pt), br(pt), tt(pt), close: true, ..style) - } else if mark == "*" or mark == "x" { - draw.line(bl(pt), tr(pt), ..style) - draw.line(tl(pt), br(pt), ..style) - } else if mark == "+" { - draw.line(ll(pt), rr(pt), ..style); - draw.line(tt(pt), bb(pt), ..style) - } else if mark == "-" { - draw.line(ll(pt), rr(pt), ..style) - } else if mark == "|" { - draw.line(tt(pt), bb(pt), ..style) - } -} - -#let draw-mark(pts, x, y, mark, mark-size, plot-size) = { - // Scale marks back to canvas scaling - let (sx, sy) = plot-size - sx = (x.max - x.min) / sx - sy = (y.max - y.min) / sy - sx *= mark-size - sy *= mark-size - - for pt in pts { - let (px, py, ..) = pt - if px >= x.min and px <= x.max and py >= y.min and py <= y.max { - draw-mark-shape(pt, (sx, sy), mark, (:)) - } - } -} diff --git a/src/lib/plot/sample.typ b/src/lib/plot/sample.typ deleted file mode 100644 index 3ad881d73..000000000 --- a/src/lib/plot/sample.typ +++ /dev/null @@ -1,79 +0,0 @@ -/// Sample the given single parameter function `samples` times, with values -/// evenly spaced within the range given by `domain` and return each -/// sampled `y` value in an array as `(x, y)` tuple. -/// -/// If the functions first return value is a tuple `(x, y)`, then all return values -/// must be a tuple. -/// -/// - fn (function): Function to sample of the form `(x) => y` or `(t) => (x, y)`, where -/// `x` or `t` are `float` values within the domain specified by `domain`. -/// - domain (domain): Domain of `fn` used as bounding interval for the sampling points. -/// - samples (int): Number of samples in domain. -/// - sample-at (array): List of x values the function gets sampled at in addition -/// to the `samples` number of samples. Values outsides the -/// specified domain are legal. -/// -> array: Array of (x, y) tuples -#let sample-fn(fn, domain, samples, sample-at: ()) = { - assert(samples + sample-at.len() >= 2, - message: "You must at least sample 2 values") - assert(type(domain) == array and domain.len() == 2, - message: "Domain must be a tuple") - - let (lo, hi) = domain - - let y0 = (fn)(lo) - let is-vector = type(y0) == array - if not is-vector { - y0 = ((lo, y0), ) - } else { - y0 = (y0, ) - } - - let pts = sample-at + range(0, samples).map(t => lo + t / (samples - 1) * (hi - lo)) - pts = pts.sorted() - - return pts.map(x => { - if is-vector { - (fn)(x) - } else { - (x, (fn)(x)) - } - }) -} - -/// Samples the given two parameter function with `x-samples` and -/// `y-samples` values evenly spaced within the range given by -/// `x-domain` and `y-domain` and returns each sampled output in -/// an array. -/// -/// - fn (function): Function of the form `(x, y) => z` with all values being numbers. -/// - x-domain (domain): Domain used as bounding interval for sampling point's x -/// values. -/// - y-domain (domain): Domain used as bounding interval for sampling point's y -/// values. -/// - x-samples (int): Number of samples in the x-domain. -/// - y-samples (int): Number of samples in the y-domain. -/// -> array: Array of z scalars -#let sample-fn2(fn, x-domain, y-domain, x-samples, y-samples) = { - assert(x-samples >= 2, - message: "You must at least sample 2 x-values") - assert(y-samples >= 2, - message: "You must at least sample 2 y-values") - assert(type(x-domain) == array and x-domain.len() == 2, - message: "X-Domain must be a tuple") - assert(type(y-domain) == array and y-domain.len() == 2, - message: "Y-Domain must be a tuple") - - let (x-min, x-max) = x-domain - let (y-min, y-max) = y-domain - let y-pts = range(0, y-samples) - let x-pts = range(0, x-samples) - - return y-pts.map(y => { - let y = y / (y-samples - 1) * (y-max - y-min) + y-min - return x-pts.map(x => { - let x = x / (x-samples - 1) * (x-max - x-min) + x-min - return float((fn)(x, y)) - }) - }) -} diff --git a/src/lib/plot/util.typ b/src/lib/plot/util.typ deleted file mode 100644 index 905fb2bcf..000000000 --- a/src/lib/plot/util.typ +++ /dev/null @@ -1,368 +0,0 @@ -#import "../../bezier.typ" - -/// Clip line-strip in rect -/// -/// - points (array): Array of vectors representing a line-strip -/// - low (vector): Lower clip-window coordinate -/// - high (vector): Upper clip-window coordinate -/// -> array List of line-strips representing the paths insides the clip-window -#let clipped-paths(points, low, high, fill: false) = { - let (min-x, max-x) = (calc.min(low.at(0), high.at(0)), - calc.max(low.at(0), high.at(0))) - let (min-y, max-y) = (calc.min(low.at(1), high.at(1)), - calc.max(low.at(1), high.at(1))) - - let in-rect(pt) = { - return (pt.at(0) >= min-x and pt.at(0) <= max-x and - pt.at(1) >= min-y and pt.at(1) <= max-y) - } - - let interpolated-end(a, b) = { - if in-rect(a) and in-rect(b) { - return b - } - - let (x1, y1, ..) = a - let (x2, y2, ..) = b - - if x2 - x1 == 0 { - return (x2, calc.min(max-y, calc.max(y2, min-y))) - } - - if y2 - y1 == 0 { - return (calc.min(max-x, calc.max(x2, min-x)), y2) - } - - let m = (y2 - y1) / (x2 - x1) - let n = y2 - m * x2 - - let x = x2 - let y = y2 - - y = calc.min(max-y, calc.max(y, min-y)) - x = (y - n) / m - - x = calc.min(max-x, calc.max(x, min-x)) - y = m * x + n - - return (x, y) - } - - // Append path to paths and return paths - // - // If path starts or ends with a vector of another part, merge those - // paths instead appending path as a new path. - let append-path(paths, path) = { - if path.len() <= 1 { - return paths - } - - let cmp(a, b) = { - return a.map(calc.round.with(digits: 8)) == b.map(calc.round.with(digits: 8)) - } - - let added = false - for i in range(0, paths.len()) { - let p = paths.at(i) - if cmp(p.first(), path.last()) { - paths.at(i) = path + p - added = true - } else if cmp(p.first(), path.first()) { - paths.at(i) = path.rev() + p - added = true - } else if cmp(p.last(), path.first()) { - paths.at(i) = p + path - added = true - } else if cmp(p.last(), path.last()) { - paths.at(i) = p + path.rev() - added = true - } - if added { break } - } - - if not added { - paths.push(path) - } - return paths - } - - let clamped-pt(pt) = { - return (calc.max(min-x, calc.min(pt.at(0), max-x)), - calc.max(min-y, calc.min(pt.at(1), max-y))) - } - - let paths = () - - let path = () - let prev = points.at(0) - let was-inside = in-rect(prev) - if was-inside { - path.push(prev) - } else if fill { - path.push(clamped-pt(prev)) - } - - for i in range(1, points.len()) { - let prev = points.at(i - 1) - let pt = points.at(i) - - let is-inside = in-rect(pt) - - let (x1, y1) = prev - let (x2, y2) = pt - - // Ignore lines if both ends are outsides the x-window and on the - // same side. - if (x1 < min-x and x2 < min-x) or (x1 > max-x and x2 > max-x) { - if fill { - let clamped = clamped-pt(pt) - if path.last() != clamped { - path.push(clamped) - } - } - was-inside = false - continue - } - - if is-inside { - if was-inside { - path.push(pt) - } else { - path.push(interpolated-end(pt, prev)) - path.push(pt) - } - } else { - if was-inside { - path.push(interpolated-end(prev, pt)) - } else { - let (a, b) = (interpolated-end(pt, prev), - interpolated-end(prev, pt)) - if in-rect(a) and in-rect(b) { - path.push(a) - path.push(b) - } else if fill { - let clamped = clamped-pt(pt) - if path.last() != clamped { - path.push(clamped) - } - } - } - - if path.len() > 0 and not fill { - paths = append-path(paths, path) - path = () - } - } - - was-inside = is-inside - } - - // Append clamped last point if filling - if fill and not in-rect(points.last()) { - path.push(clamped-pt(points.last())) - } - - if path.len() > 1 { - paths = append-path(paths, path) - } - - return paths -} - -/// Compute clipped stroke paths -/// -/// - points (array): X/Y data points -/// - low (vector): Lower clip-window coordinate -/// - high (vector): Upper clip-window coordinate -/// -> array List of stroke paths -#let compute-stroke-paths(points, low, high) = { - clipped-paths(points, low, high, fill: false) -} - -/// Compute clipped fill path -/// -/// - points (array): X/Y data points -/// - low (vector): Lower clip-window coordinate -/// - high (vector): Upper clip-window coordinate -/// -> array List of fill paths -#let compute-fill-paths(points, low, high) = { - clipped-paths(points, low, high, fill: true) -} - -/// Return points of a sampled catmull-rom through the -/// input points. -/// -/// - points (array): Array of input vectors -/// - tension (float): Catmull-Rom tension -/// - samples (int): Number of samples -/// -> array Array of vectors -#let sampled-spline-data(points, tension, samples) = { - assert(samples >= 1 and samples <= 100, - message: "Must at least use 1 sample per curve") - - let curves = bezier.catmull-to-cubic(points, tension) - let pts = () - for c in curves { - for t in range(0, samples + 1) { - let t = t / samples - pts.push(bezier.cubic-point(..c, t)) - } - } - return pts -} - -/// Simplify linear data by "detecting" linear sections -/// and skipping points until the slope changes. -/// This can have a huge impact on the number of lines -/// getting rendered. -/// -/// - data (array): Data points -/// - epsilon (float): Curvature threshold to treat data as linear -#let linearized-data(data, epsilon) = { - let pts = () - // Current slope, set to none if infinite - let dx = none - // Previous point, last skipped point - let prev = none - let skipped = none - // Current direction - let dir = 0 - - let len = data.len() - for i in range(0, len) { - let pt = data.at(i) - if prev != none and i < len - 1 { - let new-dir = pt.at(0) - prev.at(0) - if new-dir == 0 { - // Infinite slope - if dx != none { - if skipped != none {pts.push(skipped); skipped = none} - pts.push(pt) - } else { - skipped = pt - } - dx = none - } else { - // Push the previous and the current point - // if slope or direction changed - let new-dx = ((pt.at(1) - prev.at(1)) / new-dir) - if dx == none or calc.abs(new-dx - dx) > epsilon or (new-dir * dir) < 0 { - if skipped != none {pts.push(skipped); skipped = none} - pts.push(pt) - - dx = new-dx - dir = new-dir - } else { - skipped = pt - } - } - } else { - if skipped != none {pts.push(skipped); skipped = none} - pts.push(pt) - } - - prev = pt - } - - return pts -} - -// Get the default axis orientation -// depending on the axis name -#let get-default-axis-horizontal(name) = { - return lower(name).starts-with("x") -} - -// Setup axes dictionary -// -// - axis-dict (dictionary): Existing axis dictionary -// - options (dictionary): Named arguments -// - plot-size (tuple): Plot width, height tuple -#let setup-axes(ctx, axis-dict, options, plot-size) = { - import "/src/lib/axes.typ" - - // Get axis option for name - let get-axis-option(axis-name, name, default) = { - let v = options.at(axis-name + "-" + name, default: default) - if v == auto { default } else { v } - } - - for (name, axis) in axis-dict { - if not "ticks" in axis { axis.ticks = () } - axis.label = get-axis-option(name, "label", $#name$) - - // Configure axis bounds - axis.min = get-axis-option(name, "min", axis.min) - axis.max = get-axis-option(name, "max", axis.max) - - assert(axis.min not in (none, auto) and - axis.max not in (none, auto), - message: "Axis min and max must be set.") - if axis.min == axis.max { - axis.min -= 1; axis.max += 1 - } - - // Configure axis orientation - axis.horizontal = get-axis-option(name, "horizontal", - get-default-axis-horizontal(name)) - - // Configure ticks - axis.ticks.list = get-axis-option(name, "ticks", ()) - axis.ticks.step = get-axis-option(name, "tick-step", axis.ticks.step) - axis.ticks.minor-step = get-axis-option(name, "minor-tick-step", axis.ticks.minor-step) - axis.ticks.decimals = get-axis-option(name, "decimals", 2) - axis.ticks.unit = get-axis-option(name, "unit", []) - axis.ticks.format = get-axis-option(name, "format", axis.ticks.format) - - // Axis break - axis.show-break = get-axis-option(name, "break", false) - axis.inset = get-axis-option(name, "inset", (0, 0)) - - // Configure grid - axis.ticks.grid = get-axis-option(name, "grid", false) - - axis-dict.at(name) = axis - } - - // Set axis options round two, after setting - // axis bounds - for (name, axis) in axis-dict { - let changed = false - - // Configure axis aspect ratio - let equal-to = get-axis-option(name, "equal", none) - if equal-to != none { - assert.eq(type(equal-to), str, - message: "Expected axis name.") - assert(equal-to != name, - message: "Axis can not be equal to itself.") - - let other = axis-dict.at(equal-to, default: none) - assert(other != none, - message: "Other axis must exist.") - assert(other.horizontal != axis.horizontal, - message: "Equal axes must have opposing orientation.") - - let (w, h) = plot-size - let ratio = if other.horizontal { - h / w - } else { - w / h - } - axis.min = other.min * ratio - axis.max = other.max * ratio - - changed = true - } - - if changed { - axis-dict.at(name) = axis - } - } - - for (name, axis) in axis-dict { - axis-dict.at(name) = axes.prepare-axis(ctx, axis, name) - } - - return axis-dict -} diff --git a/tests/axes/ref/1.png b/tests/axes/ref/1.png deleted file mode 100644 index ddf820d1e..000000000 Binary files a/tests/axes/ref/1.png and /dev/null differ diff --git a/tests/axes/test.typ b/tests/axes/test.typ deleted file mode 100644 index d3aef27c5..000000000 --- a/tests/axes/test.typ +++ /dev/null @@ -1,57 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -// Schoolbook Axis Styling -#box(stroke: 2pt + red, canvas({ - import draw: * - - set-style(axes: ( - stroke: blue, - padding: .25, - x: (stroke: red), - y: (stroke: green, tick: (stroke: blue, length: .3)) - )) - axes.school-book(size: (6, 6), - axes.axis(min: -1, max: 1, ticks: (step: 1, minor-step: auto, - grid: "both")), - axes.axis(min: -1, max: 1, ticks: (step: .5, minor-step: auto, - grid: "major"))) -})) - -// Scientific Axis Styling -#box(stroke: 2pt + red, canvas({ - import draw: * - - set-style(axes: (stroke: blue)) - set-style(axes: (left: (tick: (stroke: green + 2pt)))) - set-style(axes: (bottom: (tick: (stroke: red, length: .5, - label: (angle: 90deg, - anchor: "east"))))) - set-style(axes: (right: (tick: (label: (offset: .2, - angle: -45deg, - anchor: "north-west"), length: -.1)))) - axes.scientific(size: (6, 6), - draw-unset: false, - top: none, - bottom: axes.axis(min: -1, max: 1, ticks: (step: 1, minor-step: auto, - grid: "both", unit: [ units])), - left: axes.axis(min: -1, max: 1, ticks: (step: .5, minor-step: auto, - grid: false)), - right: axes.axis(min: -10, max: 10, ticks: (step: auto, minor-step: auto, - grid: "major")),) -})) - -// Custom Tick Format -#box(stroke: 2pt + red, canvas({ - import draw: * - - axes.scientific(size: (6, 1), - bottom: axes.axis(min: -2*calc.pi, max: 2*calc.pi, ticks: ( - step: calc.pi, minor-step: auto, format: v => { - let d = v / calc.pi - if d == 0 {return $0$} - {$#{d}pi$} - } - )), - left: axes.axis(min: -1, max: 1, ticks: (step: none, minor-step: none))) -})) diff --git a/tests/chart/boxwhisker/ref/1.png b/tests/chart/boxwhisker/ref/1.png deleted file mode 100644 index 263b390d9..000000000 Binary files a/tests/chart/boxwhisker/ref/1.png and /dev/null differ diff --git a/tests/chart/boxwhisker/test.typ b/tests/chart/boxwhisker/test.typ deleted file mode 100644 index ecca046ae..000000000 --- a/tests/chart/boxwhisker/test.typ +++ /dev/null @@ -1,25 +0,0 @@ -#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( - size: (10, 10), - y-min: 0, - y-max: 100, - label-key: "label", - data0) -})) diff --git a/tests/chart/piechart/ref/1.png b/tests/chart/piechart/ref/1.png deleted file mode 100644 index f2de97550..000000000 Binary files a/tests/chart/piechart/ref/1.png and /dev/null differ diff --git a/tests/chart/piechart/test.typ b/tests/chart/piechart/test.typ deleted file mode 100644 index 1d10bdaa9..000000000 --- a/tests/chart/piechart/test.typ +++ /dev/null @@ -1,108 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import chart: piechart - -#let colors = gradient.linear(rgb("FFCCE5"), rgb("660033")) - -// Outset items -#box(stroke: 2pt + red, canvas({ - import draw: * - piechart(range(1,11), outset: 3, outset-offset: 25%, slice-style: colors, legend: (label: "%")) -})) - -// Outset items + inner radius -#box(stroke: 2pt + red, canvas({ - import draw: * - piechart(range(1,11), outset: 3, inner-radius: .5, outset-offset: 25%, slice-style: colors) -})) - -// Outset items + arc shape -#box(stroke: 2pt + red, canvas({ - import draw: * - piechart(range(1,5), outset-offset: 25%, slice-style: colors, - start: 0deg, stop: 180deg) -})) - -// Outset items + inner radius -#box(stroke: 2pt + red, canvas({ - import draw: * - piechart(range(1,5), inner-radius: .5, outset-offset: 25%, slice-style: colors, - start: 45deg, stop: 135deg) -})) - -// Rotated Values -#box(stroke: 2pt + red, canvas({ - piechart(range(1,11), slice-style: colors, outer-label: (angle: auto, content: "VALUE")) -})) - -// Rotated Percentages -#box(stroke: 2pt + red, canvas({ - piechart(range(10, 60, step: 10), slice-style: colors, outer-label: (angle: auto, content: "%")) -})) - -// Inner Values -#box(stroke: 2pt + red, canvas({ - piechart(range(1,11), slice-style: colors, inner-label: (content: "VALUE"), radius: 2) -})) - -// Inner Percentages -#box(stroke: 2pt + red, canvas({ - piechart(range(10, 60, step: 10), slice-style: colors, inner-label: (content: "%"), radius: 2) -})) - -// Gap as canvas size -#box(stroke: 2pt + red, canvas({ - piechart(range(1,11), gap: .1, slice-style: colors) -})) - -// Gap as canvas size + inner radius -#box(stroke: 2pt + red, canvas({ - piechart(range(1,11), gap: .1, inner-radius: .5, slice-style: colors) -})) - -// Gap as angle -#box(stroke: 2pt + red, canvas({ - piechart(range(1,11), gap: 5deg, slice-style: colors, outer-label: (angle: auto)) -})) - -// Anchors -#box(stroke: 2pt + red, canvas({ - import draw: * - piechart(range(1,11), slice-style: colors, name: "c", inner-radius: .5) - for-each-anchor("c", n => { - circle("c." + n, radius: .05) - }) -})) - -// Keys -#box(stroke: 2pt + red, canvas({ - piechart(((value: 1, label: [One], o: false), - (value: 1, label: [Two], o: true)), slice-style: colors, - value-key: "value", label-key: "label", outer-label: (content: "LABEL", radius: 150%), outset-key: "o") -})) - -// Keys -#box(stroke: 2pt + red, canvas({ - piechart(((value: 1, label: [One]), - (value: 1, label: [Two], o: 2%), - (value: 1, label: [Three], o: 4%), - (value: 1, label: [Four], o: 6%), - (value: 1, label: [Five], o: 8%), - (value: 1, label: [Six], o: 10%), - (value: 1, label: [Seven], o: 12%), - (value: 1, label: [Eight], o: 14%),), - slice-style: colors, - value-key: "value", label-key: "label", outer-label: (content: "LABEL", radius: 150%), outset-key: "o") -})) - -// Clockwise rotation -#box(stroke: 2pt + red, canvas({ - import draw: * - piechart(range(1,4), clockwise: true, slice-style: (green, yellow, red)) -})) - -// Counter clockwise rotation -#box(stroke: 2pt + red, canvas({ - import draw: * - piechart(range(1,4), clockwise: false, slice-style: (green, yellow, red)) -})) diff --git a/tests/chart/ref/1.png b/tests/chart/ref/1.png deleted file mode 100644 index 6c1ddb1a7..000000000 Binary files a/tests/chart/ref/1.png and /dev/null differ diff --git a/tests/chart/test.typ b/tests/chart/test.typ deleted file mode 100644 index 564835421..000000000 --- a/tests/chart/test.typ +++ /dev/null @@ -1,224 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let data0 = ( - ([1], 1), - ([2], 2), - ([3], 3), -) - -#let data1 = ( - ([15-24], 20.0), - ([25-29], 17.2), - ([30-34], 14.2), - ([35-44], 29.3), - ([45-54], 22.5), - ([55+], 18.4), -) - -#let data2 = ( - ([15-24], 18.0, 20.1, 23.0, 17.0), - ([25-29], 16.3, 17.6, 19.4, 15.3), - ([30-34], 14.0, 15.3, 13.9, 18.7), - ([35-44], 35.5, 26.5, 29.4, 25.8), - ([45-54], 25.0, 20.6, 22.4, 22.0), - ([55+], 19.9, 18.2, 19.2, 16.4), -) - -#let data3 = ( - (1, 0.001), - (2, 0.002), - (3, 0.003), -) - -#let data4 = ( - (1, 1, .3), - (2, 2, .2), - (3, 3, .1), -) - -#box(stroke: 2pt + red, canvas({ - chart.barchart(mode: "basic", - size: (9, auto), - data0) -})) - -#box(stroke: 2pt + red, canvas({ - chart.barchart(mode: "basic", - size: (9, auto), - value-key: 1, - label-key: 0, - x-tick-step: 5, - x-label: [x], - y-label: [y], - data1) -})) - -#box(stroke: 2pt + red, canvas({ - chart.barchart(mode: "clustered", - size: (9, auto), - label-key: 0, - value-key: (..range(1, 5)), - data2) -})) - -#box(stroke: 2pt + red, canvas({ - chart.barchart(mode: "stacked", - size: (9, auto), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -})) - -#box(stroke: 2pt + red, canvas({ - chart.barchart(mode: "stacked100", - size: (9, auto), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart(mode: "basic", - size: (auto, 5), - data0) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart(mode: "basic", - size: (auto, 5), - value-key: 1, - label-key: 0, - y-tick-step: 5, - x-label: [x], - y-label: [y], - data1) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart(mode: "clustered", - size: (auto, 5), - label-key: 0, - value-key: (..range(1, 5)), - data2) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart(mode: "stacked", - size: (auto, 5), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart(mode: "stacked100", - size: (auto, 4), - label-key: 0, - value-key: (..range(1, 5)), - bar-style: palette.blue, - data2) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart( - size: (auto, 2), - y-tick-step: .5, - y-max: 1.0, - (([$ cal(P)_+ $], 4 / 13), ([$ cal(P)_- $], 9 / 13)) - ) - - draw.set-origin((4, 0)) - - chart.barchart( - size: (3, auto), - x-tick-step: .5, - x-max: 1.0, - (([$ cal(P)_+ $], 4 / 13), ([$ cal(P)_- $], 9 / 13)) - ) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart( - size: (auto, 2), - y-tick-step: .5, - y-max: 1.0, - (([$ cal(P)_+ $], -4 / 13), ([$ cal(P)_- $], 9 / 13)) - ) - - draw.set-origin((4, 0)) - - chart.barchart( - size: (3, auto), - x-tick-step: .5, - x-max: 1.0, - (([$ cal(P)_+ $], 4 / 13), ([$ cal(P)_- $], -9 / 13)) - ) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart( - size: (auto, 2), - y-tick-step: 0.001, - y-format: "sci", - data3) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart( - size: (auto, 2), - y-tick-step: 0.001, - y-decimals: 3, - data3) -})) - -#box(stroke: 2pt + red, canvas({ - chart.barchart( - size: (5, auto), - x-tick-step: 0.001, - x-format: "sci", - data3) -})) - -#box(stroke: 2pt + red, canvas({ - chart.barchart( - size: (5, auto), - x-tick-step: 0.001, - x-decimals: 3, - data3) -})) - -#box(stroke: 2pt + red, canvas({ - draw.set-style(barchart: (bar-width: 1, cluster-gap: .2)) - chart.barchart(mode: "clustered", - size: (5, auto), - label-key: 0, - value-key: (..range(1, 5)), - data2) -})) - -#box(stroke: 2pt + red, canvas({ - draw.set-style(columnchart: (bar-width: 1, cluster-gap: .2)) - chart.columnchart(mode: "clustered", - size: (auto, 5), - label-key: 0, - value-key: (..range(1, 5)), - data2) -})) - -#box(stroke: 2pt + red, canvas({ - chart.columnchart(mode: "basic", - size: (auto, 4), - error-key: 2, - data4) -})) - -#box(stroke: 2pt + red, canvas({ - chart.barchart(mode: "basic", - size: (9, auto), - error-key: 2, - data4) -})) diff --git a/tests/plot/annotation/ref.png b/tests/plot/annotation/ref.png deleted file mode 100644 index 452e6f35f..000000000 Binary files a/tests/plot/annotation/ref.png and /dev/null differ diff --git a/tests/plot/annotation/ref/1.png b/tests/plot/annotation/ref/1.png deleted file mode 100644 index 160481af1..000000000 Binary files a/tests/plot/annotation/ref/1.png and /dev/null differ diff --git a/tests/plot/annotation/test.typ b/tests/plot/annotation/test.typ deleted file mode 100644 index 215e7cff8..000000000 --- a/tests/plot/annotation/test.typ +++ /dev/null @@ -1,23 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#box(stroke: 2pt + red, canvas({ - import draw: * - set-style(rect: (stroke: none)) - - plot.plot(size: (6, 4), { - plot.add(domain: (-calc.pi, 3*calc.pi), calc.sin) - plot.annotate(background: true, { - rect((0, -1), (calc.pi, 1), fill: blue.lighten(90%)) - rect((calc.pi, -1.1), (2*calc.pi, 1.1), fill: red.lighten(90%)) - rect((2*calc.pi, -1.5), (3.5*calc.pi, 1.5), fill: green.lighten(90%)) - }) - plot.annotate(padding: .1, { - line((calc.pi / 2, 1.1), (rel: (0, .2)), (rel: (2*calc.pi, 0)), (rel: (0, -.2))) - content((calc.pi * 1.5, 1.5), $ lambda $) - }) - plot.annotate(padding: .1, { - line((calc.pi / 2,-.1), (calc.pi / 2, .8), mark: (end: "stealth")) - }) - }) -})) diff --git a/tests/plot/bar/ref/1.png b/tests/plot/bar/ref/1.png deleted file mode 100644 index 8f185205e..000000000 Binary files a/tests/plot/bar/ref/1.png and /dev/null differ diff --git a/tests/plot/bar/test.typ b/tests/plot/bar/test.typ deleted file mode 100644 index 1a5fea841..000000000 --- a/tests/plot/bar/test.typ +++ /dev/null @@ -1,20 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#let data = ( - (0, (1, 2, 3)), - (1, (6, 7, 8), (2, 1, 0)), - (2, 5, ()), -) - -#test-case({ - import draw: * - plot.plot(size: (3, 3), x-tick-step: 1, y-tick-step: 1, - { - plot.add-bar(data, - x-key: 0, - y-key: 1, - error-key: 2) - }) -}) diff --git a/tests/plot/boxwhisker/ref/1.png b/tests/plot/boxwhisker/ref/1.png deleted file mode 100644 index e3c1081e9..000000000 Binary files a/tests/plot/boxwhisker/ref/1.png and /dev/null differ diff --git a/tests/plot/boxwhisker/test.typ b/tests/plot/boxwhisker/test.typ deleted file mode 100644 index b70a66378..000000000 --- a/tests/plot/boxwhisker/test.typ +++ /dev/null @@ -1,55 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#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: * - - plot.plot(size: (10, 10), - y-min: 0, - y-max: 100, - { - plot.add-boxwhisker((x: 1, ..box1)) - }) -})) - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (10, 10), - y-min: 0, y-max: 100, - { - plot.add-boxwhisker(( - (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), - )) - }) -})) diff --git a/tests/plot/broken-axes/ref/1.png b/tests/plot/broken-axes/ref/1.png deleted file mode 100644 index ead6a978d..000000000 Binary files a/tests/plot/broken-axes/ref/1.png and /dev/null differ diff --git a/tests/plot/broken-axes/test.typ b/tests/plot/broken-axes/test.typ deleted file mode 100644 index 19f09b2b2..000000000 --- a/tests/plot/broken-axes/test.typ +++ /dev/null @@ -1,23 +0,0 @@ -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#let data = ((5,5), (10,10)) - -#test-case({ - plot.plot(size: (8,8), - x-break: true, - y-break: true, - { - plot.add(data) - }) -}) - -#test-case({ - plot.plot(size: (8,8), - axis-style: "school-book", - x-break: true, - y-break: true, - { - plot.add(data) - }) -}) diff --git a/tests/plot/contour/ref/1.png b/tests/plot/contour/ref/1.png deleted file mode 100644 index 9af5dbfe0..000000000 Binary files a/tests/plot/contour/ref/1.png and /dev/null differ diff --git a/tests/plot/contour/test.typ b/tests/plot/contour/test.typ deleted file mode 100644 index ccb053f7f..000000000 --- a/tests/plot/contour/test.typ +++ /dev/null @@ -1,128 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let peaks(x, y) = ( - 3 * calc.pow(1 - x, 2) * calc.exp(-(x*x) - calc.pow(y + 1, 2)) - - 10 * (x/5 - calc.pow(x, 3) - calc.pow(y, 5)) * - calc.exp(-(x * x) - (y * y)) - 1/3 * calc.exp(-calc.pow(x + 1, 2) - (y * y)) -) - -/* Simple contour */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (8, 8), - x-tick-step: 5, - y-tick-step: 5, - { - plot.add-contour( - (x, y) => 2 - (x - 1) * (y - 1), - fill: true, - x-domain: (-10, 10), - y-domain: (-10, 11), - ) - - plot.add-contour( - (x, y) => 30 - (calc.pow(1 - x, 2) + calc.pow(1 - y, 2)), - fill: true, - x-domain: (-10, 10), - y-domain: (-10, 10), - ) - }) -})) - -/* Multi contour */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (8, 8), - x-tick-step: 1, - y-tick-step: 1, - { - plot.add-contour( - peaks, - z: (0, 1, 2, 3, 4), - fill: true, - x-domain: (-2, 3), - y-domain: (-2, 3), - x-samples: 50, - y-samples: 50, - ) - }) -})) - -/* Multi contour */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (8, 8), - x-tick-step: 1, - y-tick-step: 1, - { - let z(x, y) = { - (1 - x/2 + calc.pow(x,5) + calc.pow(y,3)) * calc.exp(-(x*x) - (y*y)) - } - plot.add-contour( - z, - z: (-.68, -.39, -.1, .1, .47, .76, 1.05), - fill: true, - x-domain: (-3, 3), - y-domain: (-3, 3), - x-samples: 50, - y-samples: 50, - ) - }) -})) - -/* Complex contour #270 */ -#box(stroke: 2pt + red, canvas({ - plot.plot(size: (8, 8), { - // x >= 0 - plot.add-contour( - (x, y) => x, - z: 0, - y-samples: 2, - x-samples: 2, - x-domain: (0, 10), - y-domain: (-10, 10), - fill: true, - ) - - // y >= 0 - plot.add-contour( - (x, y) => y, - z: 0, - y-samples: 2, - x-samples: 2, - x-domain: (-10, 10), - y-domain: (0, 10), - fill: true, - ) - - // hyperbola - plot.add-contour( - (x, y) => (x - 1) * (y - 1), - x-domain: (-10, 10), - y-domain: (-10, 10), - fill: true, - z: 1, - ) - - // circle - plot.add-contour( - (x, y) => (calc.pow((x - 1), 2) + calc.pow((y - 1), 2)), - x-domain: (-10, 10), - y-domain: (-10, 10), - z: 9, - op: "<=", - fill: true, - ) - - // line - plot.add-contour( - (x, y) => x + 1 - y, - x-domain: (-10, 10), - y-domain: (-10, 10), - ) - }) -})) diff --git a/tests/plot/equal-axis/ref/1.png b/tests/plot/equal-axis/ref/1.png deleted file mode 100644 index 58672e5b5..000000000 Binary files a/tests/plot/equal-axis/ref/1.png and /dev/null differ diff --git a/tests/plot/equal-axis/test.typ b/tests/plot/equal-axis/test.typ deleted file mode 100644 index 9a2fec92e..000000000 --- a/tests/plot/equal-axis/test.typ +++ /dev/null @@ -1,34 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (6,3), - x-tick-step: none, - y-tick-step: none, - x-equal: "y", - a-equal: "b", - b-horizontal: true, - { - plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t))) - plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t)), - axes: ("a", "b")) - }) -})) - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3,6), - x-tick-step: none, - y-tick-step: none, - x-equal: "y", - a-equal: "b", - b-horizontal: true, - { - plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t))) - plot.add(domain: (0, 2 * calc.pi), t => (calc.cos(t), calc.sin(t)), - axes: ("a", "b")) - }) -})) diff --git a/tests/plot/grid/ref/1.png b/tests/plot/grid/ref/1.png deleted file mode 100644 index a62f0e48c..000000000 Binary files a/tests/plot/grid/ref/1.png and /dev/null differ diff --git a/tests/plot/grid/test.typ b/tests/plot/grid/test.typ deleted file mode 100644 index 1f6fd2f0b..000000000 --- a/tests/plot/grid/test.typ +++ /dev/null @@ -1,72 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -/* X grid */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3, 3), - x-grid: true, - x-tick-step: .5, - y-tick-step: .5, - { - plot.add(((0,0), (1,1))) - }) -})) - -/* X grid */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3, 3), - x-grid: "both", - x-tick-step: .5, - x-minor-tick-step: .25, - y-tick-step: .5, - { - plot.add(((0,0), (1,1))) - }) -})) - -/* Y grid */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3, 3), - y-grid: true, - x-tick-step: .5, - y-tick-step: .5, - { - plot.add(((0,0), (1,1))) - }) -})) - -/* Y grid */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3, 3), - y-grid: "both", - x-tick-step: .5, - y-tick-step: .5, - y-minor-tick-step: .25, - { - plot.add(((0,0), (1,1))) - }) -})) - -/* X-Y grid */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3, 3), - x-grid: "both", - y-grid: "both", - x-tick-step: .5, - x-minor-tick-step: .25, - y-tick-step: .5, - y-minor-tick-step: .25, - { - plot.add(((0,0), (1,1))) - }) -})) diff --git a/tests/plot/hvline/ref/1.png b/tests/plot/hvline/ref/1.png deleted file mode 100644 index b212394fc..000000000 Binary files a/tests/plot/hvline/ref/1.png and /dev/null differ diff --git a/tests/plot/hvline/test.typ b/tests/plot/hvline/test.typ deleted file mode 100644 index a94ff06ed..000000000 --- a/tests/plot/hvline/test.typ +++ /dev/null @@ -1,59 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -/* Empty plot */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (1, 1), - x-tick-step: none, - y-tick-step: none, - { - plot.add-vline(0) - plot.add-hline(0) - plot.add(((0,0), (1, 0))) - }) -})) - -/* Line plot + h/v line */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: none, - y-tick-step: none, - { - plot.add-vline(0) - plot.add-hline(0) - plot.add(((-1, -1), (1,1))) - }) -})) - -/* Line plot + Multiple h/v lines */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: none, - y-tick-step: none, - { - plot.add-vline(-.1, 0, .1) - plot.add-hline(-.1, 0, .1) - plot.add(((-2, -2), (2,2))) - }) -})) - -/* Clipped h/v lines */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: none, - y-tick-step: none, - x-min: 0, x-max: 2, - y-min: 0, y-max: 2, - { - plot.add-vline(-.1, 1, 3) - plot.add-hline(-.1, 1, 3) - }) -})) diff --git a/tests/plot/legend/ref/1.png b/tests/plot/legend/ref/1.png deleted file mode 100644 index a5bf299e6..000000000 Binary files a/tests/plot/legend/ref/1.png and /dev/null differ diff --git a/tests/plot/legend/test.typ b/tests/plot/legend/test.typ deleted file mode 100644 index cae47b184..000000000 --- a/tests/plot/legend/test.typ +++ /dev/null @@ -1,174 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let dom = (domain: (0, 2 * calc.pi)) -#let fn(x, offset: 0) = {calc.sin(x) + offset} - -#for pos in ("north", "south", "west", "east", - "north-east", "north-west", - "south-east", "south-west",) { - block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (2, 2), - x-tick-step: none, - y-tick-step: none, - legend: pos, - { - plot.add(..dom, fn, label: $ f(x) $) - }) - })) -} - -#for pos in ("inner-north", "inner-south", "inner-west", "inner-east", - "inner-north-east", "inner-north-west", - "inner-south-east", "inner-south-west",) { - block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - legend: pos, - { - plot.add(..dom, fn, label: $ f(x) $) - }) - })) -} - -#block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(..dom, fn, label: $ f_1(x) $) - plot.add(..dom, fn.with(offset: .1), label: $ f_2(x) $) - plot.add(..dom, fn.with(offset: .2), label: $ f_3(x) $) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(..dom, fn, fn.with(offset: .5), label: $ f(x) $) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-hline(0, label: $ f(x) $) - plot.add-vline(0, label: $ f(x) $) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-contour(x-domain: (-1, 1), y-domain: (-1, 1), - (x, y) => x, z: 0, op: "<=", label: $ f(x) $) - plot.add-contour(x-domain: (-1, 1), y-domain: (-1, 1), - (x, y) => x, z: 0, fill: true, label: $ f(x) $) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - let box1 = ( - x: 1, - outliers: (7, 65, 69), - min: 15, - q1: 25, - q2: 35, - q3: 50, - max: 60) - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add-boxwhisker(box1, label: [Box]) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - set-style(legend: (item: (preview: (width: .4), spacing: .7), - orientation: ltr, default-position: "north")) - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - set-style(legend: (item: (preview: (width: .4, height: 1), spacing: 1), - padding: .1, - stroke: black, - fill: white, - orientation: ltr, default-position: "north")) - - plot.plot(size: (4, 2), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -})) - -#block(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 2), - axis-style: "school-book", - legend-style: (offset: (-2.5, 1), - item: (preview: (margin: .5), spacing: .15), - fill: white, - stroke: (paint: black, dash: "dotted"), - padding: (.1, .5)), - x-tick-step: none, - y-tick-step: none, - { - plot.add(samples: 10, ..dom, fn, mark: "o", label: $ f(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .1), mark: "x", fill: true, label: $ f_2(x) $) - plot.add(samples: 10, ..dom, fn.with(offset: .2), mark: "|", style: (stroke: none), label: $ f_3(x) $) - }) -})) diff --git a/tests/plot/line/between/ref/1.png b/tests/plot/line/between/ref/1.png deleted file mode 100644 index 75e823020..000000000 Binary files a/tests/plot/line/between/ref/1.png and /dev/null differ diff --git a/tests/plot/line/between/test.typ b/tests/plot/line/between/test.typ deleted file mode 100644 index b70a298fe..000000000 --- a/tests/plot/line/between/test.typ +++ /dev/null @@ -1,105 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let size = (6, 4) -#let f(x, y: 0) = y + calc.sin(x * 1deg) - -/* Fill between */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -})) - -/* Fill between - Clip Top */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-max: .5, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -})) - -/* Fill between - Clip Bottom */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -.5, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -})) - -/* Fill between - Clip Top & Bottom */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-max: .5, - y-min: -.5, - { - plot.add-fill-between(domain: (-360, 360), f.with(y: -1), f.with(y: 1)) - }) -})) - -/* Fill between - Test 2 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(domain: (0, 2 * calc.pi), - t => (calc.cos(t) * 1.5, calc.sin(t)), - t => (calc.cos(t), calc.sin(t) * 1.5)) - }) -})) - -/* Fill between - Test 3 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add-fill-between(domain: (0, 2 * calc.pi), - t => (calc.cos(t) * 1.5, calc.sin(t) * 1.5), - t => (calc.cos(t), calc.sin(t))) - }) -})) - -/* Fill between - Test 4 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - let f(x) = calc.sin(x) + calc.cos(3 * x) - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - // Function - plot.add(domain: (0, 4 * calc.pi), f) - // Error-Band fill - plot.add-fill-between(domain: (0, 4 * calc.pi), - style: (stroke: none), - x => f(x) - calc.exp(x/4) / 2, - x => f(x) + calc.exp(x/4) / 2) - }) -})) diff --git a/tests/plot/line/fill/ref/1.png b/tests/plot/line/fill/ref/1.png deleted file mode 100644 index 99f4d847a..000000000 Binary files a/tests/plot/line/fill/ref/1.png and /dev/null differ diff --git a/tests/plot/line/fill/test.typ b/tests/plot/line/fill/test.typ deleted file mode 100644 index b1ee6a6f1..000000000 --- a/tests/plot/line/fill/test.typ +++ /dev/null @@ -1,176 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let size = (6, 4) -#let f(x, y: 0) = y + calc.sin(x * 1deg) - -/* Epigraph/Hypograph */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - { - plot.add(domain: (-360, 360), epigraph: true, f) - plot.add(domain: (-360, 360), hypograph: true, f) - }) -})) - -/* Upper Half */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 0, - { - plot.add(domain: (-360, 360), epigraph: true, f) - plot.add(domain: (-360, 360), hypograph: true, f) - }) -})) - -/* Lower Half */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-max: 0, - { - plot.add(domain: (-360, 360), epigraph: true, f) - plot.add(domain: (-360, 360), hypograph: true, f) - }) -})) - -/* To Y=0 Clipped on Y<1 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: -.5)) - }) -})) - -/* To Y=0 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f) - }) -})) - -/* To Y=0 Clipped on Y>1 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: +.5)) - }) -})) - -/* To Y=0 Offset +1.5 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 0, y-max: 1, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: +1.5)) - }) -})) - -/* To Y=0 Offset -1.5 */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -1, y-max: 0, - { - plot.add(domain: (-360, 360), fill: true, f.with(y: -1.5)) - }) -})) - -/* To Y=0 Out of range */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 1, y-max: 2, - { - plot.add(domain: (-360, 360), fill: true, f) - }) -})) - -/* Epigraph Full Fill */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 1, y-max: 2, - { - plot.add(domain: (-360, 360), epigraph: true, f) - }) -})) - -/* Hypograph Full Fill */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -2, y-max: -1, - { - plot.add(domain: (-360, 360), hypograph: true, f) - }) -})) - -/* Epigraph No Fill */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: -2, y-max: -1, - { - plot.add(domain: (-360, 360), epigraph: true, f) - }) -})) - -/* Hypograph No Fill */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: size, - x-tick-step: none, - y-tick-step: none, - y-min: 1, y-max: 2, - { - plot.add(domain: (-360, 360), hypograph: true, f) - }) -})) diff --git a/tests/plot/line/line-type/ref/1.png b/tests/plot/line/line-type/ref/1.png deleted file mode 100644 index e5ab59175..000000000 Binary files a/tests/plot/line/line-type/ref/1.png and /dev/null differ diff --git a/tests/plot/line/line-type/test.typ b/tests/plot/line/line-type/test.typ deleted file mode 100644 index d02ae6ef7..000000000 --- a/tests/plot/line/line-type/test.typ +++ /dev/null @@ -1,23 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -/* Draw different line types */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - let data(i) = ((1, 2, 3, 4, 5).zip((1, 3, 2, 3, 1).map(v => v + i))) - plot.plot(size: (6, 6), - y-min: 0, y-max: 35, - x-tick-step: 1, - y-tick-step: 5, - { - plot.add(data(0), line: "linear", mark: "o") - plot.add(data(5), line: "spline", mark: "o") - plot.add(data(10), line: "hv", mark: "o") - plot.add(data(15), line: "vh", mark: "o") - plot.add(data(20), line: "hvh", mark: "o") - plot.add(data(25), line: (type: "hvh", mid: .25), mark: "o") - plot.add(data(30), line: (type: "hvh", mid: .75), mark: "o") - }) -})) - diff --git a/tests/plot/line/linearization/ref.png b/tests/plot/line/linearization/ref.png deleted file mode 100644 index c335f0ad3..000000000 Binary files a/tests/plot/line/linearization/ref.png and /dev/null differ diff --git a/tests/plot/line/linearization/ref/1.png b/tests/plot/line/linearization/ref/1.png deleted file mode 100644 index 2c589d2c0..000000000 Binary files a/tests/plot/line/linearization/ref/1.png and /dev/null differ diff --git a/tests/plot/line/linearization/test.typ b/tests/plot/line/linearization/test.typ deleted file mode 100644 index 81d02d362..000000000 --- a/tests/plot/line/linearization/test.typ +++ /dev/null @@ -1,26 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -/* Test linearization */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (6, 4), - { - plot.add(domain: (0, 360), x=>calc.sin(x * 1deg), - line: "raw", style: (stroke: 3pt)) - plot.add(domain: (0, 360), x=>calc.sin(x * 1deg), - line: "linear") - }) -})) - -/* Test linearization for vertical and horizontal lines */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (6, 4), - x-min: -1, x-max: 2, y-min: -1, y-max: 2, - { - plot.add(((0,0), (1,0), (1,0.1), (1,0.2), (1,0.5), (1,1), (0,1), (0,0))) - }) -})) diff --git a/tests/plot/line/mark/ref/1.png b/tests/plot/line/mark/ref/1.png deleted file mode 100644 index edf7a591e..000000000 Binary files a/tests/plot/line/mark/ref/1.png and /dev/null differ diff --git a/tests/plot/line/mark/test.typ b/tests/plot/line/mark/test.typ deleted file mode 100644 index eba737fe7..000000000 --- a/tests/plot/line/mark/test.typ +++ /dev/null @@ -1,26 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -/* Draw different marks */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (5, 4), - axis-style: "scientific", - y-max: 2, - y-min: -2, - x-tick-step: 360, - y-tick-step: 1, - style: plot.palette.red, - mark-style: plot.palette.red, - { - for (i, m) in ("o", "square", "x", "triangle", "|", "-").enumerate() { - plot.add(domain: (i * 180, (i + 1) * 180), - samples: 12, - style: (stroke: none), - mark: m, - mark-size: .3, - x => calc.sin(x * 1deg)) - } - }) -})) diff --git a/tests/plot/line/spline/ref.png b/tests/plot/line/spline/ref.png deleted file mode 100644 index 939848ee3..000000000 Binary files a/tests/plot/line/spline/ref.png and /dev/null differ diff --git a/tests/plot/line/spline/ref/1.png b/tests/plot/line/spline/ref/1.png deleted file mode 100644 index 853326322..000000000 Binary files a/tests/plot/line/spline/ref/1.png and /dev/null differ diff --git a/tests/plot/line/spline/test.typ b/tests/plot/line/spline/test.typ deleted file mode 100644 index 3e01b1b6a..000000000 --- a/tests/plot/line/spline/test.typ +++ /dev/null @@ -1,14 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -/* Draw smoothed data by using spline interpolation */ -#box(stroke: 2pt + red, canvas({ - plot.plot(size: (6, 4), - { - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: (type: "spline", tension: .40, - samples: 5)) - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: (type: "spline", tension: .47)) - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: "spline") - plot.add(((0,0), (1,1), (2,-1), (3,3)), line: (type: "spline", tension: .5)) - }) -})) diff --git a/tests/plot/mirror-axes/ref/1.png b/tests/plot/mirror-axes/ref/1.png deleted file mode 100644 index 2b86b6e64..000000000 Binary files a/tests/plot/mirror-axes/ref/1.png and /dev/null differ diff --git a/tests/plot/mirror-axes/test.typ b/tests/plot/mirror-axes/test.typ deleted file mode 100644 index c7d9dc89f..000000000 --- a/tests/plot/mirror-axes/test.typ +++ /dev/null @@ -1,12 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * -#import "/tests/helper.typ": * - -#test-case({ - // Force showing tick labels for mirrored axes - cetz.draw.set-style(axes: (tick: (label: ("show": true)))) - - cetz.plot.plot(size: (8,8), { - cetz.plot.add(domain: (0, 1), x => x) - }) -}) diff --git a/tests/plot/parametric/ref/1.png b/tests/plot/parametric/ref/1.png deleted file mode 100644 index b1924fd51..000000000 Binary files a/tests/plot/parametric/ref/1.png and /dev/null differ diff --git a/tests/plot/parametric/test.typ b/tests/plot/parametric/test.typ deleted file mode 100644 index f41d23163..000000000 --- a/tests/plot/parametric/test.typ +++ /dev/null @@ -1,92 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -/* Simple plot */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi)) - }) -})) - -/* Test clipping */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad) + .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi)) - plot.add((t) => (calc.cos(t * 1rad) - .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi)) - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) + .5), - domain: (0, 2 * calc.pi)) - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) - .5), - domain: (0, 2 * calc.pi)) - }) -})) - -/* Test filling */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi), - fill: true) - }) -})) - -/* Test clipping + filling */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - x-tick-step: 1, - y-tick-step: 1, - { - plot.add((t) => (calc.cos(t * 1rad) + .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - plot.add((t) => (calc.cos(t * 1rad) - .5, calc.sin(t * 1rad)), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) + .5), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - plot.add((t) => (calc.cos(t * 1rad), calc.sin(t * 1rad) - .5), - domain: (0, 2 * calc.pi), fill: true, fill-type: "shape") - }) -})) - -/* Test clipping + filling */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - y-max: .5, y-min: -.5, - x-max: 1, x-min: -1, - { - let f(t, off: 0) = {(calc.cos(t) / (calc.pow(calc.sin(t), 2) + 1) + off, - calc.cos(t) * calc.sin(t) / (calc.pow(calc.sin(t), 2) + 1) + off)} - plot.add(samples: 50, - domain: (0, 2 * calc.pi), f, fill:true, fill-type: "shape") - plot.add(samples: 50, - domain: (0, 2 * calc.pi), f.with(off: .4), fill:true, fill-type: "shape") - plot.add(samples: 50, - domain: (0, 2 * calc.pi), f.with(off: -.4), fill:true, fill-type: "shape") - }) -})) diff --git a/tests/plot/ref.png b/tests/plot/ref.png deleted file mode 100644 index d52620ea1..000000000 Binary files a/tests/plot/ref.png and /dev/null differ diff --git a/tests/plot/ref/1.png b/tests/plot/ref/1.png deleted file mode 100644 index 761fa266f..000000000 Binary files a/tests/plot/ref/1.png and /dev/null differ diff --git a/tests/plot/reverse-axis/ref.png b/tests/plot/reverse-axis/ref.png deleted file mode 100644 index 0c5896d56..000000000 Binary files a/tests/plot/reverse-axis/ref.png and /dev/null differ diff --git a/tests/plot/reverse-axis/ref/1.png b/tests/plot/reverse-axis/ref/1.png deleted file mode 100644 index a2d87cc7c..000000000 Binary files a/tests/plot/reverse-axis/ref/1.png and /dev/null differ diff --git a/tests/plot/reverse-axis/test.typ b/tests/plot/reverse-axis/test.typ deleted file mode 100644 index d059193cd..000000000 --- a/tests/plot/reverse-axis/test.typ +++ /dev/null @@ -1,20 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (10, 10), x-min: 9, x-max: 0, - { - plot.add(domain: (0, 9), calc.sqrt) - }) -})) - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (10, 10), y-min: 9, y-max: 0, - { - plot.add(domain: (-5, 5), x => calc.pow(x, 2)) - }) -})) diff --git a/tests/plot/sample/sample.typ b/tests/plot/sample/sample.typ deleted file mode 100644 index 54d617e34..000000000 --- a/tests/plot/sample/sample.typ +++ /dev/null @@ -1,37 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let cases = ( - (samples: 2, res: ((0,0), (100,10))), - (samples: 5, res: ((0,0), (25,2.5), (50,5.0), (75,7.5), (100,10.0))), - (samples: 2, res: ((0,0), (50,5.0), (60,6.0), (100,10)), extra: (50,60)), -) -#for c in cases { - let pts = plot.sample-fn(x => x/10, (0, 100), c.samples, - sample-at: c.at("extra", default: ())) - assert.eq(pts, c.res, - message: "Expected: " + repr(c.res) + ", got: " + repr(pts)) -} - -#let cases = ( - (samples: (2,2), res: (( 0,100), - (100,200))), - (samples: (3,3), res: (( 0, 50,100), - ( 50,100,150), - (100,150,200))), -) -#for c in cases { - let rows = plot.sample-fn2((x, y) => x + y, (0, 100), (0,100), - c.samples.at(0), c.samples.at(1)) - assert.eq(rows, c.res, - message: "Expected: " + repr(c.res) + ", got: " + repr(rows)) -} - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (3, 1), axis-style: none, { - plot.add(domain: (0, 100), x => 0, mark: "x", samples: 2) - plot.add(domain: (0, 100), x => 1, mark: "x", samples: 5) - }) -})) diff --git a/tests/plot/test.typ b/tests/plot/test.typ deleted file mode 100644 index c998414ef..000000000 --- a/tests/plot/test.typ +++ /dev/null @@ -1,306 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#let line-data = ((-1,-1), (1,1),) - -#let data = (..(for x in range(-360, 360 + 1) { - ((x, calc.sin(x * 1deg)),) -})) - -/* Scientific Style */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (5, 2), - x-tick-step: 180, - y-tick-step: 1, - x-grid: "major", - y-grid: "major", - { - plot.add(data) - }) -})) - -/* 4-Axes */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (5, 3), - x-tick-step: 180, - x-min: -360, - x-max: 360, - y-tick-step: 1, - x2-label: none, - x2-min: -90, - x2-max: 90, - x2-tick-step: 45, - x2-minor-tick-step: 15, - y2-label: none, - y2-min: -1.5, - y2-max: 1.5, - y2-tick-step: .5, - y2-minor-tick-step: .1, - { - plot.add(data) - plot.add(data, style: (stroke: blue), axes: ("x2", "y2")) - }) -})) - -/* School-Book Style */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (5, 4), - axis-style: "school-book", - x-tick-step: 180, - y-tick-step: 1, - { - plot.add(data) - }) -})) - -/* Clipping */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (5, 4), - axis-style: "school-book", - x-min: auto, - x-max: 350, - x-tick-step: 180, - y-min: -.5, - y-max: .5, - y-tick-step: 1, - { - plot.add(data) - }) -})) - -/* Palettes */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (5, 4), - x-label: [Rainbow], - x-tick-step: none, - axis-style: "scientific", - y-label: [Color], - y-max: 8, - y-tick-step: none, - { - for i in range(0, 7) { - plot.add(domain: (i * 180, (i + 1) * 180), - epigraph: true, - style: plot.palette.rainbow, - x => calc.sin(x * 1deg)) - } - }) -})) - -/* Tick Step Calculation */ -#box(stroke: 2pt + red, {canvas({ - import draw: * - - plot.plot(size: (12, 4), - y2-decimals: 4, - { - plot.add(((0,0), (1,10)), axes: ("x", "y")) - plot.add(((0,0), (.1,.01)), axes: ("x2", "y2")) - }) -}); canvas({ - import draw: * - - plot.plot(size: (12, 4), - y2-decimals: 9, - x2-decimals: 9, - y2-format: "sci", - { - plot.add(((0,0), (30,2500)), axes: ("x", "y")) - plot.add(((0,0), (.001,.0001)), axes: ("x2", "y2")) - }) -})}) - -/* Axis Styles */ -#box(stroke: 2pt + red, stack(dir: ltr, - ..("scientific", "left", "school-book").map(axis-style => { - canvas({ - import draw: * - plot.plot(size: (4,4), x-tick-step: 90, y-tick-step: 1, - axis-style: axis-style, { - plot.add(domain: (0, 360), x => calc.sin(x * 1deg)) - }) - }) - }) -)) - -/* Manual Axis Bounds */ -#let circle-data = range(0, 361).map( - t => (.5 * calc.cos(t*1deg), .5 * calc.sin(t*1deg))) -#box(stroke: 2pt + red, stack(dir: ltr, canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - xl-min: -1.5, xl-max: .5, - xr-min: -.5, xr-max: 1.5, - yb-min: -1.5, yb-max: .5, - yt-min: -.5, yt-max: 1.5, - { - plot.add(circle-data) - plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) - plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) - plot.add(circle-data, axes: ("x", "yt"), style: (stroke: blue)) - plot.add(circle-data, axes: ("x", "yb"), style: (stroke: yellow)) - }) -}), canvas({ - import draw: * - - plot.plot(size: (4, 4), - x-tick-step: 1, - y-tick-step: 1, - x-min: -1, x-max: 1, - y-min: -1, y-max: 1, - xl-min: -1.75, xl-max: .25, - xr-min: -.25, xr-max: 1.75, - yb-min: -1.75, yb-max: .25, - yt-min: -.25, yt-max: 1.75, - { - plot.add(circle-data) - plot.add(circle-data, axes: ("xl", "y"), style: (stroke: green)) - plot.add(circle-data, axes: ("xr", "y"), style: (stroke: red)) - plot.add(circle-data, axes: ("x", "yt"), style: (stroke: blue)) - plot.add(circle-data, axes: ("x", "yb"), style: (stroke: yellow)) - }) -}),)) - -/* Anchors */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (5, 3), name: "plot", - x-tick-step: 180, - y-tick-step: 1, - x-grid: "major", - y-grid: "major", - { - plot.add(data, fill: true) - plot.add-anchor("from", (-270, "max")) - plot.add-anchor("to", (90, "max")) - plot.add-anchor("lo", (90, 0)) - plot.add-anchor("hi", (90, "max")) - }) - - line((rel: (0, .2), to: "plot.from"), - (rel: (0, .2), to: "plot.to"), - mark: (start: "|", end: "|"), name: "annotation") - content((rel: (0, .1), to: ("annotation.start", 50%, "annotation.end")), $2 pi$, anchor: "south") - - line((rel: (0, .2), to: "plot.lo"), - (rel: (0, -.2), to: "plot.hi"), - mark: (start: ">", end: ">"), name: "amplitude") -})) - -/* Custom sample points */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (6, 4), y-min: -2, y-max: 2, - samples: 10, - { - plot.add(samples: 2, sample-at: (.99, 1.001, 1.99, 2.001, 2.99), domain: (0, 3), - x => calc.pow(-1, int(x))) - }) -})) - -/* Format tick values */ -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (6, 4), - x-tick-step: none, - x-ticks: (-1, 0, 1), - x-format: x => $x_(#x)$, - y-tick-step: none, - y-ticks: (-1, 0, 1), - y-format: x => $y_(#x)$, - x2-tick-step: none, - x2-ticks: (-1, 0, 1), - x2-format: x => $x_(2,#x)$, - y2-tick-step: none, - y2-ticks: (-1, 0, 1), - y2-format: x => $y_(2,#x)$, - { - plot.add(samples: 2, domain: (-1, 1), x => -x, axes: ("x", "y")) - plot.add(samples: 2, domain: (-1, 1), x => x, axes: ("x2", "y2")) - }) -})) - -// Test plot with anchors only -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (6, 4), name: "plot", - x-min: -1, x-max: 1, y-min: -1, y-max: 1, - { - plot.add-anchor("test", (0,0)) - }) - circle("plot.test", radius: 1) -})) - -// Test empty plot -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (1, 1), {}) -})) - -// Some axis styling -#box(stroke: 2pt + red, canvas({ - import draw: * - - set-style(axes: ( - padding: .1, - tick: ( - length: -.1, - ), - left: ( - stroke: (paint: red), - tick: ( - stroke: auto, - ) - ), - bottom: ( - stroke: (paint: blue, thickness: 2pt), - tick: ( - stroke: auto, - ) - ), - )) - - plot.plot(size: (6, 4), axis-style: "scientific-auto", { - plot.add(line-data) - }) - - set-origin((7, 0)) - - set-style(axes: ( - overshoot: .5, - x: ( - padding: 1, - overshoot: -.5, - stroke: blue, - ), - y: ( - stroke: red, - ) - )) - plot.plot(size: (6, 4), axis-style: "school-book", - x-tick-step: none, - y-tick-step: none, - { - plot.add(line-data) - }) -})) diff --git a/tests/plot/vertical/ref/1.png b/tests/plot/vertical/ref/1.png deleted file mode 100644 index 6cbfb6fe8..000000000 Binary files a/tests/plot/vertical/ref/1.png and /dev/null differ diff --git a/tests/plot/vertical/test.typ b/tests/plot/vertical/test.typ deleted file mode 100644 index e01d03c9b..000000000 --- a/tests/plot/vertical/test.typ +++ /dev/null @@ -1,49 +0,0 @@ -#set page(width: auto, height: auto) -#import "/src/lib.typ": * - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (10, 10), - { - plot.add(domain: (0, 4*calc.pi), calc.sin, axes: ("y", "x")) - }) -})) - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (10, 10), - { - plot.add-contour(x-domain: (0, 4), y-domain: (-2, 2), - (x, y) => x - .5 * y, op: ">=", z: 2, axes: ("y", "x"), fill: true) - }) -})) - -#box(stroke: 2pt + red, canvas({ - import draw: * - - let box1 = ( - outliers: (7, 65, 69), - min: 15, - q1: 25, - q2: 35, - q3: 50, - max: 60) - - plot.plot(size: (10, 10), - { - plot.add-boxwhisker((x: 1, ..box1), axes: ("y", "x")) - }) -})) - -#box(stroke: 2pt + red, canvas({ - import draw: * - - plot.plot(size: (10, 10), y-label: $ x $, - x-label: $ y $, - x-min: -.75, x-max: .75, - { - plot.add(domain: (0, 4*calc.pi), calc.sin, axes: ("y", "x")) - }) -}))