diff --git a/packages/preview/riesketcher/0.4.0/.gitignore b/packages/preview/riesketcher/0.4.0/.gitignore new file mode 100644 index 0000000000..348a0ebdd4 --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/.gitignore @@ -0,0 +1,2 @@ +*.pdf +!manual.pdf \ No newline at end of file diff --git a/packages/preview/riesketcher/0.4.0/LICENSE b/packages/preview/riesketcher/0.4.0/LICENSE new file mode 100644 index 0000000000..f03920df4e --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-2024 Kainoa Kanter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/riesketcher/0.4.0/README.md b/packages/preview/riesketcher/0.4.0/README.md new file mode 100644 index 0000000000..b48daba8a8 --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/README.md @@ -0,0 +1,15 @@ +# riesketcher + +A package to draw Riemann sums (and their plots) of a function with CeTZ. + +Usage example and docs: [manual.pdf](https://github.com/ThatOneCalculator/riesketcher/blob/main/manual.pdf) + +```typst +#import "@preview/riesketcher:0.4.0": riesketcher +``` + +![Demo](https://github.com/ThatOneCalculator/riesketcher/assets/44733677/4f87b750-e4be-4698-b650-74f4fe56789d) + +![Demo for custom partitions](https://github.com/VincentTam/riesketcher/assets/5748535/3b830cb8-c017-4ed4-9410-7f3bad79edab) + +![Demo for trapezoidal rule](https://github.com/user-attachments/assets/f72b6a96-2c8e-4b95-a4d1-cad89fbb75e0) diff --git a/packages/preview/riesketcher/0.4.0/lib.typ b/packages/preview/riesketcher/0.4.0/lib.typ new file mode 100644 index 0000000000..3aed54db00 --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/lib.typ @@ -0,0 +1,2 @@ +#import "riesketcher.typ": riesketcher +#import "trapezoidal.typ": trapezoidal diff --git a/packages/preview/riesketcher/0.4.0/manual.pdf b/packages/preview/riesketcher/0.4.0/manual.pdf new file mode 100644 index 0000000000..13a7477087 Binary files /dev/null and b/packages/preview/riesketcher/0.4.0/manual.pdf differ diff --git a/packages/preview/riesketcher/0.4.0/manual.typ b/packages/preview/riesketcher/0.4.0/manual.typ new file mode 100644 index 0000000000..74337ab26f --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/manual.typ @@ -0,0 +1,125 @@ +#import "@preview/tidy:0.1.0" +#import "@preview/cetz:0.4.1": canvas +#import "lib.typ": riesketcher, trapezoidal +// #import "@preview/riesketcher:0.4.0": riesketcher, trapezoidal + +#set text(size: 10.5pt) + += Riesketcher +A package to draw Riemann sums (left, right, midpoint, and trapezoidal) and their plots for functions using CeTZ; supports tagged and untagged partitions. +```typst +#import "@preview/riesketcher:0.4.0": riesketcher, trapezoidal +``` + +#show raw.where(lang: "example"): it => block({ + table(columns: (50%, 50%), stroke: none, align: (center + horizon, left), + align(left, raw(lang: "typc", it.text)), + eval("canvas({" + it.text + "})", scope: (canvas: canvas, riesketcher: riesketcher, trapezoidal: trapezoidal)) + ) +}) + +== Examples +=== Left-Hand Riemann sum + +```example +riesketcher( + x => calc.pow(x, 3) + 4, + method: "left", + start: -3.1, + end: 3.5, + n: 10, + plot-x-tick-step: 1, +) +``` + +=== Midpoint Riemann sum + +```example +riesketcher( + x => -calc.pow(x, 2) + 9, + method: "mid", + domain: (-4, 4), + start: -3, + end: 3, + n: 6, + plot-x-tick-step: 1, +) +``` + +=== Right-method Riemann sum + +```example +riesketcher( + x => 16 - x * x, + method: "right", + end: 6, + n: 6, + domain: (-1, auto), + plot-x-tick-step: 1, +) +``` + +=== Custom untagged partition (midpoint method) + +```example +riesketcher( + x => 0.17 * calc.pow(x, 3) + + 1.5 * calc.sin(calc.cos(x)), + method: "mid", + partition: (-3, -1.5, -0.75, -0.2, 0.8, 1.5, + 2.3, 3.4), + plot-x-tick-step: 2, +) +``` + +=== Tagged partition + +```example +riesketcher( + x => 0.5 * calc.pow(x, 3) + - 0.9 * calc.cos(x), + partition: (-3.2, -2.1, -1.1, 0.4, 0.9, 1.7, + 2.4, 3.5), + tags: (-2.5, -1.9, -0.35, 0.63, 1.38, 2.06, 3.14), + plot-x-tick-step: 2, +) +``` + +#pagebreak() + +=== Trapezoidal Rule (uniform grid) + +```example +trapezoidal( + x => calc.pow(x, 3) + 4, + start: -3, + end: 3.5, + n: 7, + plot-x-tick-step: 1, + positive-color: rgb("#210aa4") +) +``` + +=== Trapezoidal Rule (non-uniform grid) + +```example +trapezoidal( + x => -calc.pow(x, 3) - 4, + partition: (-3, -0.4, 2, 3.1, 3.5), + plot-x-tick-step: 1, + positive-color: rgb("#210aa4") +) +``` + +#pagebreak() +#set align(left) + +== Method parameters + +#let riesketcher-tidy = tidy.parse-module(read("riesketcher.typ"), name: "riesketcher") +#tidy.show-module(riesketcher-tidy) + +#pagebreak() + +#let trapezoidal-tidy = tidy.parse-module(read("trapezoidal.typ"), name: "trapezoidal") +#tidy.show-module(trapezoidal-tidy) diff --git a/packages/preview/riesketcher/0.4.0/riesketcher.typ b/packages/preview/riesketcher/0.4.0/riesketcher.typ new file mode 100644 index 0000000000..f71eaff127 --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/riesketcher.typ @@ -0,0 +1,160 @@ +#import "@preview/cetz-plot:0.1.2" +#import "util.typ": _validate-partition, _validate-tagged-partition + +/// Draw a Riemann sum of a function, and optionally plot the function. +/// +/// - fn (function): The function to draw a Riemann sum of. +/// - domain (array): Tuple of the domain of `fn`. If a tuple value is `auto`, that +/// value is set to `start`/`end`. +/// - start (number): The starting point for the bars. Used only if `partition` +/// is not a valid array; otherwise, the first value of `partition` is used. +/// - end (number): The ending point for the bars. Used only if `partition` is +/// not a valid array; otherwise, the last value of `partition` is used. +/// - n (number): Number of bars. Used only if `partition` is not a valid array; +/// otherwise, the number of bars is determined by the length of `partition`. +/// - method (string): Determines where the sample points for bar heights are +/// taken (`left`, `mid`/`midpoint`, or `right`). Used only if `tags` is not a +/// valid array; otherwise, bar heights are taken from `tags`. +/// - partition (array, none): (optional) Array of partition points. If valid, it +/// overrides `start`, `end`, and `n`; otherwise, equal partitions are +/// generated from `start`, `end`, and `n`. +/// - tags (array, none): (optional) Array of sample points for bar heights. If +/// valid, it overrides `method`; otherwise, sample points are determined by +/// `method`. +/// - transparency (number): Transparency fill of bars. +/// - dot-radius (number): Radius of dots. +/// - plot (boolean): Whether to add plot of the function. +/// - plot-grid (boolean): Show grid on plot. +/// - plot-x-tick-step (number): X tick step of plot. +/// - plot-y-tick-step (number): Y tick step of plot. +/// - positive-color (color): Color of positive bars. +/// - negative-color (color): Color of negative bars. +/// - plot-line-color (color): Color of plotted line. +/// - size (tuple): The width and height of the plot area, given as a tuple +/// `(width, height)`. Controls the overall size of the rendered Riemann sum +/// and function plot. +#let riesketcher( + fn, + start: 0, + end: 10, + domain: (auto, auto), + n: 10, + partition: none, + tags: none, + method: "left", + transparency: 40%, + dot-radius: 0.15, + plot: true, + plot-grid: false, + plot-x-tick-step: auto, + plot-y-tick-step: auto, + positive-color: color.green, + negative-color: color.red, + plot-line-color: color.blue, + size: (5, 5), +) = { + let delta-x = 0 // store fallback bar width if partition is invalid + + // check if partition is valid + let is-valid-partition = _validate-partition(partition) + if is-valid-partition { + start = partition.at(0) + end = partition.at(-1) + n = partition.len() - 1 + } else { + delta-x = (end - start) / n + partition = range(0, n + 1).map(k => start + delta-x * k) + } + + // Adjust the function domain if set to auto + if domain.at(0) == auto { domain.at(0) = start } + if domain.at(1) == auto { domain.at(1) = end } + + // calculate bar widths + let bar-widths = if is-valid-partition { + range(0, partition.len() - 1).map(k => partition.at(k + 1) - partition.at(k)) + } else { + range(0, n).map(x => delta-x) + } + + // check if tags are valid + let is-valid-tags = _validate-tagged-partition(partition, tags) + + // calculate bar heights + let bar-y = none + if is-valid-tags { + bar-y = tags.zip(tags.map(fn)) + } else { + let horizontal-hand-offset = 0% + if method == "right" { + horizontal-hand-offset = 100% + } + else if method == "mid" or method == "midpoint" { + horizontal-hand-offset = 50% + } + bar-y = range(0, n).map(k => { + let x = partition.at(k) + bar-widths.at(k) * horizontal-hand-offset / 100% + (x, fn(x)) + }) + } + + let col-trans(color, opacity) = { + let space = color.space() + space(..color.components(alpha: false), opacity) + } + + let positive-bar-style = ( + fill: col-trans(positive-color.lighten(70%).darken(8%), transparency), + stroke: col-trans(positive-color.darken(30%), 90%) + 1.1pt + ) + let negative-bar-style = ( + : ..positive-bar-style, + fill: col-trans(negative-color.lighten(70%).darken(8%), transparency), + stroke: col-trans(negative-color.darken(30%), 90%) + 1.1pt + ) + let positive-dot-style = ( + stroke: black, + fill: positive-color + ) + let negative-dot-style = ( + : ..positive-dot-style, + fill: negative-color, + ) + + cetz-plot.plot.plot( + size: size, + x-grid: plot-grid, + y-grid: plot-grid, + x-label: $x$, + y-label: $y$, + axis-style: if plot { "school-book" } else { none }, + x-tick-step: plot-x-tick-step, + y-tick-step: plot-y-tick-step, + { + for k in range(0, n) { + let x = partition.at(k) + let y = bar-y.at(k).at(1) + cetz-plot.plot.add-bar(((x, y),), + bar-width: bar-widths.at(k), + bar-position: "start", + style: if y >= 0 { positive-bar-style } else { negative-bar-style }) + } + + if plot { + cetz-plot.plot.add( + domain: domain, + x => fn(x), + style: (stroke: plot-line-color + 1.5pt)) + } + + for k in range(0, n) { + let (x, y) = bar-y.at(k) + cetz-plot.plot.add(((x, y),), + mark: "o", + style: (stroke: none), + mark-size: dot-radius, + mark-style: if y >= 0 { positive-dot-style } else { negative-dot-style }) + } + } + ) +} diff --git a/packages/preview/riesketcher/0.4.0/trapezoidal.typ b/packages/preview/riesketcher/0.4.0/trapezoidal.typ new file mode 100644 index 0000000000..88dbf56aec --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/trapezoidal.typ @@ -0,0 +1,142 @@ +#import "@preview/cetz-plot:0.1.2" +#import "util.typ": _validate-partition + +/// Illustrate the chained trapezoidal rule of a function, and optionally plot the +/// function. +/// +/// - fn (function): The function to illustrate the chained trapezoidal rule of. +/// - domain (array): Tuple of the domain of `fn`. If a tuple value is `auto`, that +/// value is set to `start`/`end`. +/// - start (number): The starting point for the trapezoids. Used only if +/// `partition` is not a valid array; otherwise, the first value of `partition` +/// is used. +/// - end (number): The ending point for the trapezoids. Used only if `partition` +/// is not a valid array; otherwise, the last value of `partition` is used. +/// - n (number): Number of trapezoids. Used only if `partition` is not a valid +/// array; otherwise, the number of trapezoids is determined by the length of +/// `partition`. +/// - partition (array, none): (optional) Array of partition points. If valid, it +/// overrides `start`, `end`, and `n`; otherwise, equal partitions are generated +/// from `start`, `end`, and `n`. +/// - transparency (number): Transparency fill of trapezoids. +/// - dot-radius (number): Radius of dots. +/// - plot (boolean): Whether to add plot of the function. +/// - plot-grid (boolean): Show grid on plot. +/// - plot-x-tick-step (number): X tick step of plot. +/// - plot-y-tick-step (number): Y tick step of plot. +/// - positive-color (color): Color of positive trapezoids. +/// - negative-color (color): Color of negative trapezoids. +/// - plot-line-color (color): Color of plotted line. +/// - size (tuple): The width and height of the plot area, given as a tuple +/// `(width, height)`. Controls the overall size of the rendered Riemann sum +/// and function plot. +#let trapezoidal( + fn, + start: 0, + end: 10, + domain: (auto, auto), + n: 10, + partition: none, + transparency: 40%, + dot-radius: 0.15, + plot: true, + plot-grid: false, + plot-x-tick-step: auto, + plot-y-tick-step: auto, + positive-color: color.green, + negative-color: color.red, + plot-line-color: color.blue, + size: (5, 5), +) = { + // check if partition is valid + let is-valid-partition = _validate-partition(partition) + if is-valid-partition { + start = partition.at(0) + end = partition.at(-1) + n = partition.len() - 1 + } else { + let delta-x = (end - start) / n + partition = range(0, n + 1).map(k => start + delta-x * k) + } + + // Adjust the function domain if set to auto + if domain.at(0) == auto { domain.at(0) = start } + if domain.at(1) == auto { domain.at(1) = end } + + // calculate data points + let data-points = partition.zip(partition.map(fn)) + + let col-trans(color, opacity) = { + let space = color.space() + space(..color.components(alpha: false), opacity) + } + + let positive-trapezoid-style = ( + fill: col-trans(positive-color.lighten(30%), transparency), + stroke: col-trans(positive-color.darken(30%), 90%) + 1.1pt + ) + let negative-trapezoid-style = ( + : ..positive-trapezoid-style, + fill: col-trans(negative-color.lighten(30%), transparency), + stroke: col-trans(negative-color.darken(30%), 90%) + 1.1pt + ) + let positive-dot-style = ( + stroke: black, + fill: positive-color + ) + let negative-dot-style = ( + : ..positive-dot-style, + fill: negative-color, + ) + + cetz-plot.plot.plot( + size: size, + x-grid: plot-grid, + y-grid: plot-grid, + x-label: $x$, + y-label: $y$, + axis-style: if plot { "school-book" } else { none }, + x-tick-step: plot-x-tick-step, + y-tick-step: plot-y-tick-step, + { + // plot curve + if plot { + cetz-plot.plot.add( + domain: domain, + x => fn(x), + style: (stroke: (paint: plot-line-color, thickness: 1.5pt, dash: "dashed")) + ) + } + + // then plot trapezoids + for k in range(n) { + let (x0, y0) = data-points.at(k) + let (x1, y1) = data-points.at(k + 1) + let subdata = ((x0, y0), (x1, y1)) + if (y0 >= 0 and y1 < 0) or (y0 < 0 and y1 >= 0) { + let x-intersect = x0 + (x1 - x0) * (0 - y0) / (y1 - y0) + subdata.insert(1, (x-intersect, 0)) + } + for p in range(subdata.len() - 1) { + let (xl, yl) = subdata.at(p) + let (xr, yr) = subdata.at(p + 1) + cetz-plot.plot.add(((xl, 0), (xl, yl), (xr, yr), (xr, 0)), + style: if yl > 0 or yr > 0 { positive-trapezoid-style } else { negative-trapezoid-style }, + fill: true + ) + } + } + + // plot dots + for point in data-points { + let (x, y) = point + cetz-plot.plot.add((point,), + mark: "o", + style: (stroke: none), + mark-size: dot-radius, + mark-style: if y >= 0 { positive-dot-style } else { negative-dot-style } + ) + } + } + ) +} diff --git a/packages/preview/riesketcher/0.4.0/typst.toml b/packages/preview/riesketcher/0.4.0/typst.toml new file mode 100644 index 0000000000..36c8d4abbb --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/typst.toml @@ -0,0 +1,10 @@ +[package] +name = "riesketcher" +version = "0.4.0" +entrypoint = "lib.typ" +authors = ["Kainoa Kanter"] +license = "MIT" +description = "A package to draw Riemann sums (and their plots) of a function with CeTZ." +repository = "https://github.com/ThatOneCalculator/riesketcher" +exclude = ["manual.typ", "manual.pdf"] +keywords = ["riemann", "integral", "graph", "calculus", "cetz"] diff --git a/packages/preview/riesketcher/0.4.0/util.typ b/packages/preview/riesketcher/0.4.0/util.typ new file mode 100644 index 0000000000..f81e5b7e6d --- /dev/null +++ b/packages/preview/riesketcher/0.4.0/util.typ @@ -0,0 +1,32 @@ +// check if array has duplicates +#let _check-dup(arr) = { + for i in range(0, arr.len() - 1) { + if arr.at(i) == arr.at(i + 1) { + return true + } + } + return false +} + +#let _is-num(x) = type(x) == int or type(x) == float + +// check if partition is valid +#let _validate-partition(partition) = { + if partition != none and partition.len() > 1 and partition.all(_is-num) { + let sorted-partition = partition.sorted() + return partition == sorted-partition and not _check-dup(partition) + } + return false +} + +// check if a tagged partition is valid +#let _validate-tagged-partition(partition, tags) = { + if _validate-partition(partition) and tags != none and tags.len() > 0 and partition.len() == tags.len() + 1 and tags.all(_is-num) { + let sorted-tags = tags.sorted() + if tags == sorted-tags and not _check-dup(tags) { + let zipped = (partition.zip(tags), partition.at(-1)).flatten() + return zipped == zipped.sorted() + } + } + return false +}