diff --git a/packages/preview/simple-plot/0.2.6/LICENSE b/packages/preview/simple-plot/0.2.6/LICENSE new file mode 100644 index 0000000000..7081613f31 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Nathan + +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/simple-plot/0.2.6/README.md b/packages/preview/simple-plot/0.2.6/README.md new file mode 100644 index 0000000000..0dcf37b718 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/README.md @@ -0,0 +1,521 @@ +# simple-plot + +A simple, pgfplots-like function plotting library for Typst. Create beautiful mathematical plots with minimal code. + +> **Note:** This package is built on top of [CeTZ](https://github.com/cetz-package/cetz) v0.4.2. + +## Manual + +A full manual is available in [docs/manual.pdf](docs/manual.pdf), with a Typst source version in [docs/manual.typ](docs/manual.typ) + +## Gallery + +Click on an image to see the source code. + +| | | | +|:---:|:---:|:---:| +| [![Parabola](gallery/parabola.png)](gallery/parabola.typ) | [![Trigonometric Functions](gallery/trig-functions.png)](gallery/trig-functions.typ) | [![Scatter Plot](gallery/scatter.png)](gallery/scatter.typ) | +| Parabola | Trigonometric Functions | Scatter Plot | +| [![Exponential](gallery/exponential.png)](gallery/exponential.typ) | [![Data Fit](gallery/data-fit.png)](gallery/data-fit.typ) | [![Markers](gallery/markers.png)](gallery/markers.typ) | +| Exponential & Logarithmic | Data with Model Fit | Marker Types | +| [![Extended Axes](gallery/extended-axes.png)](gallery/extended-axes.typ) | | | +| Extended Axes | | | + +## Features + +- **Simple API** - Plot functions with just a few lines of code +- **Multiple plot types** - Functions, scatter plots, line plots with markers +- **Customizable axes** - Position, labels, ticks, and tick labels +- **Axis extension** - Extend axes beyond plot area for cleaner appearance +- **Grid support** - Major and minor grids with custom styling +- **14 marker types** - Circles, squares, triangles, diamonds, stars, and more +- **Origin label control** - Toggle origin '0' label display +- **Global defaults** - Set defaults for all plots in your document +- **Full styling** - Customize colors, strokes, backgrounds, and more + +## Quick Start + +```typst +#import "@preview/simple-plot:0.2.6": plot + +#plot( + xmin: -3, xmax: 3, + ymin: -1, ymax: 9, + xlabel: $x$, + ylabel: $y$, + show-grid: true, + (fn: x => calc.pow(x, 2), stroke: blue + 1.5pt), +) +``` + +## Basic Usage + +### Plotting Functions + +```typst +#import "@preview/simple-plot:0.2.6": plot + +// Single function +#plot( + xmin: -5, xmax: 5, + ymin: -5, ymax: 5, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + (fn: x => calc.sin(x), stroke: blue + 1.5pt), +) + +// Multiple functions +#plot( + xmin: -2 * calc.pi, xmax: 2 * calc.pi, + ymin: -1.5, ymax: 1.5, + (fn: x => calc.sin(x), stroke: blue + 1.2pt, label: $sin(x)$), + (fn: x => calc.cos(x), stroke: red + 1.2pt, label: $cos(x)$), +) +``` + +### Scatter Plots + +```typst +#import "@preview/simple-plot:0.2.6": plot, scatter + +#plot( + xmin: 0, xmax: 10, + ymin: 0, ymax: 10, + show-grid: true, + scatter( + ((1, 2), (2, 3.5), (3, 2.8), (4, 5.2), (5, 4.8)), + mark: "*", + mark-fill: blue, + ), +) +``` + +### Line Plots with Markers + +```typst +#import "@preview/simple-plot:0.2.6": plot, line-plot + +#plot( + xmin: 0, xmax: 10, + ymin: 0, ymax: 12, + axis-x-pos: "bottom", + axis-y-pos: "left", + line-plot( + ((0, 0), (1, 0.5), (2, 1.8), (3, 4.2), (4, 5.1)), + stroke: blue + 1.2pt, + mark: "*", + mark-fill: blue, + ), +) +``` + +### Function Labels with Positioning + +Control the placement of function labels to avoid overlapping with your graphs using `label-pos` and `label-side`: + +```typst +#import "@preview/simple-plot:0.2.6": plot + +#plot( + xmin: -5, xmax: 5, + ymin: -3, ymax: 5, + show-grid: true, + // Label positioned at 90% along the curve, to the right + (fn: x => 0.2 * calc.pow(x, 2) - 2, + stroke: blue + 1.5pt, + label: $f(x)$, + label-pos: 0.9, // Position along curve (0-1) + label-side: "below-right" // Placement relative to point + ), + // Label positioned at 20% along the curve, above and to the right + (fn: x => -0.5 * x + 1, + stroke: red + 1.5pt, + label: $g(x)$, + label-pos: 0.2, + label-side: "above-right" + ), +) +``` + +**Available label-side options:** +- `"above"`, `"below"`, `"left"`, `"right"` - Basic 4 directions +- `"above-left"`, `"above-right"`, `"below-left"`, `"below-right"` - Diagonal positions + +The `label-pos` parameter (0-1) determines where along the function curve the label appears, while `label-side` controls the anchor point to prevent overlapping with the graph line. + +## Mathematical Functions + +Functions use Typst's `calc` module: + +| Function | Typst syntax | +|----------|--------------| +| Power $x^n$ | `calc.pow(x, n)` | +| Square root | `calc.sqrt(x)` | +| Absolute value | `calc.abs(x)` | +| Sine, Cosine, Tangent | `calc.sin(x)`, `calc.cos(x)`, `calc.tan(x)` | +| Exponential $e^x$ | `calc.exp(x)` | +| Natural log | `calc.ln(x)` | +| Log base b | `calc.log(x, base: b)` | + +> **Important:** Use decimal notation for constants (e.g., `2.0` instead of `2`) to avoid type errors: +> - ✓ `x => x * x / 2.0` +> - ✗ `x => x * x / 2` + +## Parameters Reference + +### Plot Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `xmin`, `xmax` | float | -5, 5 | X-axis range | +| `ymin`, `ymax` | float | -5, 5 | Y-axis range | +| `width`, `height` | float | 6, 6 | Plot size in cm | +| `scale` | float | 1 | Scale factor for the entire plot | +| `xlabel`, `ylabel` | content | none | Axis labels | +| `show-grid` | bool/str | false | Grid display: `true`, `false`, `"major"`, `"minor"`, `"both"` | +| `minor-grid-step` | int | 5 | Minor grid subdivisions per major tick | +| `grid-label-break` | bool | true | Draw white boxes behind labels to break grid lines | +| `axis-x-pos` | float/str | 0 | X-axis position: value, `"bottom"`, `"center"` | +| `axis-y-pos` | float/str | 0 | Y-axis position: value, `"left"`, `"center"` | +| `axis-x-extend` | float/array | (0, 0.5) | Extend X-axis beyond grid: value or `(left, right)` | +| `axis-y-extend` | float/array | (0, 0.5) | Extend Y-axis beyond grid: value or `(bottom, top)` | +| `show-origin` | bool | true | Show "0" label at origin | +| `unit-label-only` | bool | false | Show only "1" on axes for minimal style | +| `tick-label-size` | length | 10pt | Font size for tick labels | +| `axis-label-size` | length | 10pt | Font size for axis labels (x, y) | + +### Axis Label Placement + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `xlabel-pos` | str/array | "end" | Position: `"end"`, `"center"`, or `(x, y)` | +| `ylabel-pos` | str/array | "end" | Position: `"end"`, `"center"`, or `(x, y)` | +| `xlabel-anchor` | str | "west" | Text anchor point | +| `ylabel-anchor` | str | "south" | Text anchor point | +| `xlabel-offset` | array | (0.3, 0) | Offset `(x, y)` in cm | +| `ylabel-offset` | array | (0, 0.3) | Offset `(x, y)` in cm | + +### Tick Configuration + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `xtick`, `ytick` | auto/none/array | auto | Tick positions | +| `xtick-step`, `ytick-step` | auto/float | 1 | Step between ticks (integers by default) | +| `xtick-label-step`, `ytick-label-step` | int | 1 | Show label every N ticks | +| `xtick-labels`, `ytick-labels` | auto/array | auto | Custom tick labels | + +### Function Specification + +Each function is a dictionary with: + +```typst +( + fn: x => ..., // Required: the function + stroke: blue + 1.2pt, // Line style + domain: (min, max), // Optional: restrict domain + samples: 100, // Number of sample points + label: $f(x)$, // Optional label + label-pos: 0.8, // Label position (0-1) + label-side: "above", // Label placement: "above", "below", "left", "right", + // "above-left", "above-right", "below-left", "below-right" + mark: "o", // Marker type + mark-size: 0.1, // Marker size + mark-fill: white, // Marker fill color + mark-stroke: blue, // Marker stroke + mark-interval: 10, // Show marker every N points +) +``` + +## Marker Types + +| Type | Description | Type | Description | +|------|-------------|------|-------------| +| `"o"` | Hollow circle | `"*"` | Filled circle | +| `"square"` | Hollow square | `"square*"` | Filled square | +| `"triangle"` | Hollow triangle | `"triangle*"` | Filled triangle | +| `"diamond"` | Hollow diamond | `"diamond*"` | Filled diamond | +| `"star"` | Hollow star | `"star*"` | Filled star | +| `"+"` | Plus sign | `"x"` | Cross | +| `"|"` | Vertical bar | `"-"` | Horizontal bar | +| `"none"` | No marker | | | + +## Custom Styling + +```typst +#plot( + // ... + style: ( + background: ( + fill: white, + stroke: black + 0.5pt, + ), + axis: ( + stroke: black + 1pt, + arrow: "stealth", + ), + grid: ( + major: (stroke: gray + 0.6pt), + minor: (stroke: gray.lighten(50%) + 0.3pt), + ), + ticks: ( + length: 0.1, + stroke: black + 0.6pt, + label-offset: 0.15, + label-size: 0.8em, + ), + plot: ( + stroke: blue + 1.2pt, + samples: 100, + ), + marker: ( + size: 0.12, + stroke: black + 0.8pt, + fill: black, + ), + ), +) +``` + +## Setting Global Defaults + +Set defaults that apply to all subsequent plots: + +```typst +#import "@preview/simple-plot:0.2.6": plot, set-plot-defaults, reset-plot-defaults + +// Set defaults +#set-plot-defaults( + width: 6, + height: 4, + show-grid: "major", + xlabel: $x$, + ylabel: $y$, +) + +// All plots now use these defaults +#plot(xmin: -2, xmax: 2, ymin: 0, ymax: 4, + (fn: x => calc.pow(x, 2))) + +#plot(xmin: -3, xmax: 3, ymin: -1, ymax: 1, + (fn: x => calc.sin(x))) + +// Override specific values +#plot(width: 10, xlabel: $t$, + xmin: 0, xmax: 5, + (fn: x => 2*x)) + +// Reset all defaults +#reset-plot-defaults() +``` + +## Examples + +### Trigonometric Functions + +```typst +#plot( + xmin: -2 * calc.pi, xmax: 2 * calc.pi, + ymin: -2, ymax: 2, + width: 10, height: 5, + show-grid: "major", + xtick: (-2*calc.pi, -calc.pi, 0, calc.pi, 2*calc.pi), + xtick-labels: ($-2pi$, $-pi$, $0$, $pi$, $2pi$), + (fn: x => calc.sin(x), stroke: blue + 1.2pt), + (fn: x => calc.cos(x), stroke: red + 1.2pt), +) +``` + +### Exponential and Logarithmic + +```typst +#plot( + xmin: -2, xmax: 3, + ymin: -2, ymax: 5, + show-grid: true, + (fn: x => calc.exp(x), stroke: green + 1.5pt, label: $e^x$), + (fn: x => if x > 0 { calc.ln(x) } else { float.nan }, + domain: (0.01, 3), stroke: orange + 1.5pt, label: $ln(x)$), +) +``` + +For powers with an arbitrary base (e.g. $2^x$, $3^x$), use `calc.pow(base, x)`: + +```typst +#plot( + xmin: -2, xmax: 5, + ymin: 0, ymax: 32, + show-grid: true, + (fn: x => calc.pow(2, x), stroke: blue + 1.5pt, label: $2^x$), + (fn: x => calc.pow(3, x), stroke: red + 1.5pt, label: $3^x$), +) +``` + +For logarithms with an arbitrary base, use `calc.log(x, base: b)` (or `calc.ln(x)` for the natural log): + +```typst +#plot( + xmin: 0, xmax: 10, + ymin: -2, ymax: 4, + show-grid: true, + (fn: x => calc.ln(x), + domain: (0.01, 10), stroke: green + 1.5pt, label: $ln(x)$), + (fn: x => calc.log(x, base: 2), + domain: (0.01, 10), stroke: blue + 1.5pt, label: $log_2(x)$), + (fn: x => calc.log(x, base: 10), + domain: (0.01, 10), stroke: red + 1.5pt, label: $log_(10)(x)$), +) +``` + +### Piecewise Functions + +```typst +#plot( + xmin: -3, xmax: 4, + ymin: -1, ymax: 5, + show-grid: true, + (fn: x => if x < 0 { calc.pow(x, 2) } else { calc.sqrt(x) }, + stroke: blue + 1.5pt), +) +``` + +### Experimental Data with Fit + +```typst +#import "@preview/simple-plot:0.2.6": plot, line-plot + +#plot( + xmin: 0, xmax: 10, + ymin: 0, ymax: 12, + xlabel: [Time (s)], + ylabel: [Distance (m)], + show-grid: true, + axis-x-pos: "bottom", + axis-y-pos: "left", + // Experimental data + line-plot( + ((0, 0), (1, 0.5), (2, 1.8), (3, 4.2), (4, 5.1), (5, 6.8)), + mark: "*", + mark-fill: blue, + label: [Data], + ), + // Theoretical fit + (fn: x => 0.15 * calc.pow(x, 2) + 0.3 * x, + stroke: red + 1pt, label: [Model]), +) +``` + +### Extended Axes + +Make axes extend beyond the plot area for a cleaner look: + +```typst +#plot( + xmin: -5, xmax: 5, + ymin: -5, ymax: 5, + show-grid: true, + // Extend axes by 0.5 units in each direction + axis-x-extend: 0.5, + axis-y-extend: 0.5, + (fn: x => calc.pow(x, 2) / 5, stroke: blue + 1.5pt), +) + +// Asymmetric extension: (left/bottom, right/top) +#plot( + xmin: 0, xmax: 10, + ymin: 0, ymax: 8, + axis-x-pos: "bottom", + axis-y-pos: "left", + axis-x-extend: (0, 1), // Only extend right + axis-y-extend: (0, 1), // Only extend top + (fn: x => calc.sqrt(x) * 2, stroke: green + 1.5pt), +) +``` + +## Comparison with Other Plotting Libraries + +### When to use simple-plot + +**simple-plot** is designed for mathematical function plotting with a focus on simplicity and ease of use. Choose simple-plot when you need to: + +- **Plot mathematical functions** quickly with minimal boilerplate code +- **Create publication-quality plots** for math, physics, or engineering documents +- **Use a familiar API** similar to pgfplots/matplotlib for straightforward plotting tasks +- **Get started fast** with sensible defaults and intuitive parameter names + +### Alternatives + +- **[cetz-plot](https://typst.app/universe/package/cetz-plot/)**: A comprehensive charting library for data visualization including pie charts, bar charts, pyramid charts, and process diagrams. Better suited for business charts and general data visualization than mathematical function plotting. + +- **[lilaq](https://typst.app/universe/package/lilaq/)**: A powerful, feature-rich plotting library with advanced capabilities like colormesh, contour plots, multi-axis support, and quiver plots. Ideal for complex scientific visualizations, but has a steeper learning curve and requires more setup code. + +**In summary**: Use simple-plot for straightforward mathematical function plotting, cetz-plot for business charts and data visualization, and lilaq for advanced scientific plotting with complex multi-axis layouts. + +## Dependencies + +- [CeTZ](https://github.com/cetz-package/cetz) (v0.4.2+) + +## License + +MIT License - see LICENSE file for details. + +## Changelog + +All notable changes to simple-plot are documented here. + +### [Unreleased] - 2026-02-11 + +#### Changed +- Internal refactor in tick label placement to consistently reuse the computed `label-offset` value. + +### [0.2.6] - 2026-02-04 + +#### Added +- **Grid label breaks**: White boxes behind tick labels create elegant breaks in grid lines (enabled by default) +- **Integer ticks by default**: Tick step defaults to 1 for cleaner integer labels +- **Tick label step**: `xtick-label-step` and `ytick-label-step` to show labels only at every N-th tick +- **Unit label only mode**: `unit-label-only: true` shows only "1" on axes for minimal style +- **Axis arrows extend beyond grid**: Axes now extend 0.5 units beyond the grid by default on the arrow side +- Axis labels (x, y) now position at the extended arrow tips + +#### Changed +- Default tick label size changed from `0.65em` to `10pt` (same as axis labels) +- Default axis label size changed from `0.8em` to `10pt` for consistent sizing +- Grid lines no longer extend beyond the plot bounds (only axes extend) +- Minor grid step default changed to 5 subdivisions + +#### Fixed +- White box positioning for grid label breaks now properly accounts for text dimensions and minus signs + +### [0.2.5] - 2026-01-27 + +#### Fixed +- Function `label-pos` now respects explicit function domains; when no domain is set, it falls back to the axis range + +### [0.2.0] - 2026-01-15 + +#### Added +- `axis-x-extend` parameter to extend X-axis beyond plot area (symmetric or asymmetric) +- `axis-y-extend` parameter to extend Y-axis beyond plot area (symmetric or asymmetric) +- `show-origin` parameter to control display of "0" label at origin +- `label-side` parameter for function labels with 8 positioning options: "above", "below", "left", "right", "above-left", "above-right", "below-left", "below-right" +- Line segment clipping using Liang-Barsky algorithm for cleaner plot rendering +- New gallery example: `extended-axes.typ` demonstrating axis extension + +#### Fixed +- Tick numbering algorithm now generates more intuitive automatic tick intervals +- Function label positioning now correctly uses visible area instead of extended sampling domain + +### [0.1.0] - 2026-01-13 + +#### Added +- Initial release with core plotting functionality +- Function plotting with customizable domains and sampling +- Scatter plots and line plots with markers +- 14 marker types (circles, squares, triangles, diamonds, stars, plus, cross, bars) +- Customizable axes with flexible positioning +- Grid support (major, minor, both) +- Tick configuration with auto-generation and custom labels +- Function labels with flexible positioning +- Global defaults system with set/reset functions +- Full styling customization for all plot elements +- Built on CeTZ v0.4.2 diff --git a/packages/preview/simple-plot/0.2.6/docs/manual.pdf b/packages/preview/simple-plot/0.2.6/docs/manual.pdf new file mode 100644 index 0000000000..148f50e56c Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/docs/manual.pdf differ diff --git a/packages/preview/simple-plot/0.2.6/docs/manual.typ b/packages/preview/simple-plot/0.2.6/docs/manual.typ new file mode 100644 index 0000000000..6ba15a1409 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/docs/manual.typ @@ -0,0 +1,1164 @@ +#import "@preview/simple-plot:0.2.6": plot, plot-fn, scatter, line-plot, func-plot, set-plot-defaults, reset-plot-defaults + +// ============================================================================= +// DOCUMENT SETUP +// ============================================================================= + +#set page(margin: (x: 1.8cm, y: 2cm)) +#set text(size: 10.5pt, font: "New Computer Modern") +#set heading(numbering: "1.") +#set par(justify: true) + +#show raw.where(lang: "typst"): it => block( + fill: luma(97%), + radius: 3pt, + inset: 8pt, + stroke: 0.5pt + luma(85%), +)[#it] + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +/// Two-column example layout with code and preview +#let example(code, body) = block(breakable: false)[ + #table( + columns: (1fr, 1fr), + stroke: none, + inset: 6pt, + align: (left + top, center + top), + [ + #set text(size: 8pt) + *Code* + #v(0.3em) + #code + ], + [ + *Preview* + #v(0.3em) + #box( + width: 100%, + inset: 6pt, + radius: 3pt, + stroke: 0.5pt + luma(85%), + fill: white, + )[ + #set text(size: 9pt) + #body + ] + ], + ) + #v(0.5em) +] + +/// Full-width example for larger plots +#let example-full(code, body) = block(breakable: false)[ + #set text(size: 8pt) + *Code* + #v(0.3em) + #code + #v(0.5em) + *Preview* + #v(0.3em) + #align(center)[ + #box( + inset: 8pt, + radius: 3pt, + stroke: 0.5pt + luma(85%), + fill: white, + )[ + #body + ] + ] + #v(0.8em) +] + +// ============================================================================= +// TITLE PAGE +// ============================================================================= + +#align(center)[ + #v(2cm) + #text(size: 28pt, weight: "bold")[simple-plot] + #v(0.5em) + #text(size: 16pt)[Typst Package] + #v(1em) + #text(size: 12pt, style: "italic")[Mathematical Function Plotting] + #v(2cm) + #line(length: 60%, stroke: 0.5pt) + #v(1cm) + #text(size: 11pt)[ + A lightweight library for creating elegant mathematical plots\ + Version 0.2.6\ + Nathan Scheinmann + ] +] + +#pagebreak() + +// ============================================================================= +// TABLE OF CONTENTS +// ============================================================================= + +#outline(indent: 1em, depth: 2) + +#pagebreak() + +// ============================================================================= +// INTRODUCTION +// ============================================================================= + += Introduction + +`simple-plot` is a Typst package for creating clean, elegant mathematical plots. Built on CeTZ, it provides an intuitive interface for plotting functions, data points, and creating publication-ready graphs. + +== Features + +- Plot mathematical functions with automatic sampling +- Scatter plots and line plots with customizable markers +- Clean integer-based tick system by default +- Major and minor grid with elegant styling +- White box masking for tick labels (grid-label-break) +- Automatic axis extension beyond grid +- Flexible axis positioning (origin, bottom/left, custom) +- Multiple label display options (unit-label-only, label-step) +- Function labels with flexible positioning +- Clipping for clean rendering at boundaries + +== Installation + +Import the package in your Typst document: + +```typst +#import "@preview/simple-plot:0.2.6": plot +``` + +== Quick Start + +#example-full( + [```typst +#plot( + width: 6, height: 5, + xmin: -3, xmax: 3, ymin: -2, ymax: 4, + xlabel: $x$, ylabel: $y$, + show-grid: true, + (fn: x => x * x, stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 6, height: 5, + xmin: -3, xmax: 3, ymin: -2, ymax: 4, + xlabel: $x$, ylabel: $y$, + show-grid: true, + (fn: x => x * x, stroke: blue + 1.5pt), + ) + ] +) + +#pagebreak() + +// ============================================================================= +// BASIC USAGE +// ============================================================================= + += Basic Usage + +== Plotting Functions + +Plot mathematical functions by passing a dictionary with `fn`: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: -4, xmax: 4, ymin: -1.5, ymax: 1.5, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + (fn: x => calc.sin(x), stroke: blue + 1.5pt), + (fn: x => calc.cos(x), stroke: red + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: -4, xmax: 4, ymin: -1.5, ymax: 1.5, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + (fn: x => calc.sin(x), stroke: blue + 1.5pt), + (fn: x => calc.cos(x), stroke: red + 1.5pt), + ) + ] +) + +== Mathematical Functions Reference + +Functions are defined using Typst's `calc` module. Here are the most common mathematical functions: + +#table( + columns: (1fr, 1fr), + stroke: 0.5pt + luma(80%), + inset: 6pt, + fill: (_, y) => if y == 0 { luma(95%) } else { none }, + [*Function*], [*Typst syntax*], + [Power $x^n$], [`calc.pow(x, n)`], + [Square root $sqrt(x)$], [`calc.sqrt(x)`], + [Absolute value $|x|$], [`calc.abs(x)`], + [Sine $sin(x)$], [`calc.sin(x)`], + [Cosine $cos(x)$], [`calc.cos(x)`], + [Tangent $tan(x)$], [`calc.tan(x)`], + [Exponential $e^x$], [`calc.exp(x)`], + [Natural log $ln(x)$], [`calc.ln(x)`], + [Log base $b$], [`calc.log(x, base: b)`], + [Maximum], [`calc.max(a, b)`], + [Minimum], [`calc.min(a, b)`], +) + +#v(0.5em) + +#block( + fill: rgb("#fff3cd"), + stroke: rgb("#ffc107") + 0.5pt, + radius: 3pt, + inset: 8pt, +)[ + *Important:* When using constants in calculations, use decimal notation (e.g., `2.0` instead of `2`) to avoid type errors. For example: + + - ✓ `x => x * x / 2.0` + - ✗ `x => x * x / 2` _(may cause errors)_ + + This is because Typst's type system requires consistent float arithmetic. +] + +#pagebreak() + +== Function Domain + +Specify a custom domain for functions: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: 0, xmax: 5, ymin: 0, ymax: 3, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "major", + (fn: x => calc.sqrt(x), domain: (0, 5), stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: 0, xmax: 5, ymin: 0, ymax: 3, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "major", + (fn: x => calc.sqrt(x), domain: (0, 5), stroke: blue + 1.5pt), + ) + ] +) + +#pagebreak() + +== Function Labels + +Add labels to your functions: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: -3, xmax: 3, ymin: -2, ymax: 4, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + ( + fn: x => x * x, + stroke: blue + 1.5pt, + label: $f(x) = x^2$, + label-side: "above", + label-pos: 0.75, + ), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: -3, xmax: 3, ymin: -2, ymax: 4, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + ( + fn: x => x * x, + stroke: blue + 1.5pt, + label: $f(x) = x^2$, + label-side: "above", + label-pos: 0.75, + ), + ) + ] +) + +== Data Points and Scatter Plots + +Plot discrete data points: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: 0, xmax: 6, ymin: 0, ymax: 10, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "major", + ( + data: ((1, 2), (2, 4), (3, 5), (4, 7), (5, 9)), + mark: "o", + mark-size: 0.15, + stroke: blue + 1pt, + ), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: 0, xmax: 6, ymin: 0, ymax: 10, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "major", + ( + data: ((1, 2), (2, 4), (3, 5), (4, 7), (5, 9)), + mark: "o", + mark-size: 0.15, + stroke: blue + 1pt, + ), + ) + ] +) + +#pagebreak() + +// ============================================================================= +// GRID OPTIONS +// ============================================================================= + += Grid Options + +== Grid Modes + +Control grid display with `show-grid`: + +#example( + [```typst +// Major grid only +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + show-grid: "major", +) + +// Minor grid only +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + show-grid: "minor", +) + +// Both grids +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + show-grid: "both", +) + ```], + [ + #plot(width: 3.5, height: 3, xmin: -2, xmax: 2, ymin: -2, ymax: 2, show-grid: "major") + #v(0.3em) + #plot(width: 3.5, height: 3, xmin: -2, xmax: 2, ymin: -2, ymax: 2, show-grid: "both") + ] +) + +== Minor Grid Subdivisions + +Control the number of subdivisions with `minor-grid-step`: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: 0, xmax: 4, ymin: 0, ymax: 3, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "both", + minor-grid-step: 10, // 10 subdivisions per unit + (fn: x => calc.sqrt(x), domain: (0, 4), stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: 0, xmax: 4, ymin: 0, ymax: 3, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "both", + minor-grid-step: 10, + (fn: x => calc.sqrt(x), domain: (0, 4), stroke: blue + 1.5pt), + ) + ] +) + +#pagebreak() + +== Grid Label Break + +The `grid-label-break` option (enabled by default) draws white boxes behind tick labels, creating an elegant effect where grid lines appear to stop at the labels: + +#example-full( + [```typst +#plot( + width: 10, height: 8, + xmin: -4, xmax: 4, ymin: -3, ymax: 3, + xlabel: $x$, ylabel: $y$, + show-grid: "both", + minor-grid-step: 5, + grid-label-break: true, // Default + (fn: x => calc.sin(x) * 2, stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 10, height: 8, + xmin: -4, xmax: 4, ymin: -3, ymax: 3, + xlabel: $x$, ylabel: $y$, + show-grid: "both", + minor-grid-step: 5, + grid-label-break: true, + (fn: x => calc.sin(x) * 2, stroke: blue + 1.5pt), + ) + ] +) + +#pagebreak() + +// ============================================================================= +// AXIS CONFIGURATION +// ============================================================================= + += Axis Configuration + +== Axis Position + +Position axes at origin (default), bottom/left, or custom values: + +#example( + [```typst +// Through origin (default) +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + show-grid: "major", +) + +// Bottom and left +#plot( + width: 5, height: 4, + xmin: 0, xmax: 4, ymin: 0, ymax: 3, + axis-x-pos: "bottom", + axis-y-pos: "left", + show-grid: "major", +) + ```], + [ + #plot(width: 3.5, height: 3, xmin: -2, xmax: 2, ymin: -2, ymax: 2, show-grid: "major") + #v(0.5em) + #plot(width: 3.5, height: 3, xmin: 0, xmax: 4, ymin: 0, ymax: 3, axis-x-pos: "bottom", axis-y-pos: "left", show-grid: "major") + ] +) + +== Axis Extension + +By default, axes extend 0.5 units beyond the grid on the arrow side. Customize with `axis-x-extend` and `axis-y-extend`: + +#example( + [```typst +// Default extension (0, 0.5) +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + show-grid: "major", +) + +// Custom extension +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + axis-x-extend: (0.5, 1), + axis-y-extend: (0.5, 1), + show-grid: "major", +) + ```], + [ + #plot(width: 3.5, height: 3, xmin: -2, xmax: 2, ymin: -2, ymax: 2, show-grid: "major") + #v(0.5em) + #plot(width: 3.5, height: 3, xmin: -2, xmax: 2, ymin: -2, ymax: 2, axis-x-extend: (0.5, 1), axis-y-extend: (0.5, 1), show-grid: "major") + ] +) + +#pagebreak() + +// ============================================================================= +// TICK CONFIGURATION +// ============================================================================= + += Tick Configuration + +== Default Integer Ticks + +By default, ticks are placed at every integer (step = 1): + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: -3, xmax: 4, ymin: -2, ymax: 3, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + (fn: x => x, stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: -3, xmax: 4, ymin: -2, ymax: 3, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + (fn: x => x, stroke: blue + 1.5pt), + ) + ] +) + +== Custom Tick Step + +Change tick spacing with `xtick-step` and `ytick-step`: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: 0, xmax: 3, ymin: 0, ymax: 2, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + xtick-step: 0.5, + ytick-step: 0.5, + show-grid: "major", + (fn: x => x * x / 3, stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: 0, xmax: 3, ymin: 0, ymax: 2, + xlabel: $x$, ylabel: $y$, + axis-x-pos: "bottom", axis-y-pos: "left", + xtick-step: 0.5, + ytick-step: 0.5, + show-grid: "major", + (fn: x => x * x / 3, stroke: blue + 1.5pt), + ) + ] +) + +#pagebreak() + +== Tick Label Step + +Show labels only at every N-th tick with `xtick-label-step` and `ytick-label-step`: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: -6, xmax: 6, ymin: -4, ymax: 4, + xlabel: $x$, ylabel: $y$, + xtick-label-step: 2, // Labels at -6, -4, -2, 2, 4, 6 + ytick-label-step: 2, // Labels at -4, -2, 2, 4 + show-grid: "major", + (fn: x => calc.sin(x) * 3, stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: -6, xmax: 6, ymin: -4, ymax: 4, + xlabel: $x$, ylabel: $y$, + xtick-label-step: 2, + ytick-label-step: 2, + show-grid: "major", + (fn: x => calc.sin(x) * 3, stroke: blue + 1.5pt), + ) + ] +) + +== Unit Label Only + +Show only "1" on each axis for a minimal style: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: -3, xmax: 3, ymin: -3, ymax: 3, + xlabel: $x$, ylabel: $y$, + unit-label-only: true, + show-origin: false, + show-grid: "major", + (fn: x => x * x - 1, stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: -3, xmax: 3, ymin: -3, ymax: 3, + xlabel: $x$, ylabel: $y$, + unit-label-only: true, + show-origin: false, + show-grid: "major", + (fn: x => x * x - 1, stroke: blue + 1.5pt), + ) + ] +) + +#pagebreak() + +== Custom Tick Positions + +Specify exact tick positions with `xtick` and `ytick`: + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: -4, xmax: 4, ymin: -2, ymax: 2, + xlabel: $x$, ylabel: $y$, + xtick: (-calc.pi, -calc.pi/2, 0, calc.pi/2, calc.pi), + xtick-labels: ($-pi$, $-pi/2$, $0$, $pi/2$, $pi$), + show-grid: "major", + (fn: x => calc.sin(x), stroke: blue + 1.5pt), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: -4, xmax: 4, ymin: -2, ymax: 2, + xlabel: $x$, ylabel: $y$, + xtick: (-calc.pi, -calc.pi/2, 0, calc.pi/2, calc.pi), + xtick-labels: ($-pi$, $-pi/2$, $0$, $pi/2$, $pi$), + show-grid: "major", + (fn: x => calc.sin(x), stroke: blue + 1.5pt), + ) + ] +) + +== Hide Origin Label + +Control the "0" label at the origin: + +#example( + [```typst +// With origin (default) +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + show-origin: true, + show-grid: "major", +) + +// Without origin +#plot( + width: 5, height: 4, + xmin: -2, xmax: 2, ymin: -2, ymax: 2, + show-origin: false, + show-grid: "major", +) + ```], + [ + #plot(width: 3.5, height: 3, xmin: -2, xmax: 2, ymin: -2, ymax: 2, show-origin: true, show-grid: "major") + #v(0.5em) + #plot(width: 3.5, height: 3, xmin: -2, xmax: 2, ymin: -2, ymax: 2, show-origin: false, show-grid: "major") + ] +) + +#pagebreak() + +// ============================================================================= +// MARKERS +// ============================================================================= + += Markers + +== Available Marker Types + +The following markers are available: + +#table( + columns: (1fr, 2fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Marker*], [*Description*], + [`"o"`], [Circle (outline)], + [`"*"`], [Circle (filled)], + [`"square"` / `"s"`], [Square (outline)], + [`"square*"`], [Square (filled)], + [`"triangle"` / `"^"`], [Triangle up (outline)], + [`"triangle*"`], [Triangle up (filled)], + [`"diamond"` / `"d"`], [Diamond (outline)], + [`"diamond*"`], [Diamond (filled)], + [`"star"`], [Star (outline)], + [`"star*"`], [Star (filled)], + [`"+"`], [Plus sign], + [`"x"`], [Cross], + [`"|"`], [Vertical bar], + [`"-"`], [Horizontal bar], +) + +== Using Markers + +#example-full( + [```typst +#plot( + width: 8, height: 6, + xmin: 0, xmax: 7, ymin: 0, ymax: 5, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "major", + (data: ((1, 1), (2, 2), (3, 2.5)), mark: "o", stroke: blue), + (data: ((1, 2), (2, 3), (3, 3.5)), mark: "square*", stroke: red), + (data: ((1, 3), (2, 4), (3, 4.2)), mark: "triangle", stroke: green), +) + ```], + [ + #plot( + width: 8, height: 6, + xmin: 0, xmax: 7, ymin: 0, ymax: 5, + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "major", + (data: ((1, 1), (2, 2), (3, 2.5)), mark: "o", mark-size: 0.12, stroke: blue), + (data: ((1, 2), (2, 3), (3, 3.5)), mark: "square*", mark-size: 0.12, stroke: red), + (data: ((1, 3), (2, 4), (3, 4.2)), mark: "triangle", mark-size: 0.12, stroke: green), + ) + ] +) + +#pagebreak() + +// ============================================================================= +// CONVENIENCE FUNCTIONS +// ============================================================================= + += Convenience Functions + +== `plot-fn` - Quick Function Plot + +Plot a single function with automatic y-scaling: + +#example-full( + [```typst +#plot-fn( + x => calc.sin(x) * 2.0, + domain: (-4, 4), + stroke: blue + 1.5pt, +) + ```], + [ + #plot-fn( + x => calc.sin(x) * 2.0, + domain: (-4, 4), + stroke: blue + 1.5pt, + ) + ] +) + +== `scatter` - Scatter Plot Helper + +Create scatter plot specifications: + +```typst +#let my-data = scatter( + ((1, 2), (2, 4), (3, 5)), + mark: "o", + stroke: blue, +) +#plot(xmin: 0, xmax: 4, ymin: 0, ymax: 6, my-data) +``` + +== `line-plot` - Line Plot Helper + +Create connected line plots: + +```typst +#let my-line = line-plot( + ((1, 2), (2, 4), (3, 5)), + stroke: blue + 1pt, + mark: "o", +) +#plot(xmin: 0, xmax: 4, ymin: 0, ymax: 6, my-line) +``` + +== `func-plot` - Function Plot Helper + +Create function plot specifications: + +```typst +#let my-func = func-plot( + x => calc.sin(x), + stroke: blue + 1.5pt, + label: $sin(x)$, +) +#plot(xmin: -4, xmax: 4, ymin: -1.5, ymax: 1.5, my-func) +``` + +#pagebreak() + +// ============================================================================= +// GLOBAL CONFIGURATION +// ============================================================================= + += Global Configuration + +== Setting Defaults + +Use `set-plot-defaults` to configure defaults for all subsequent plots: + +```typst +#set-plot-defaults( + width: 8, + height: 6, + show-grid: "both", + minor-grid-step: 5, +) + +// All plots will now use these defaults +#plot(xmin: -3, xmax: 3, ymin: -2, ymax: 2, ...) +``` + +== Resetting Defaults + +Reset to original defaults: + +```typst +#reset-plot-defaults() +``` + +#pagebreak() + +// ============================================================================= +// STYLING +// ============================================================================= + += Styling + +== Custom Styles + +Override default styles with the `style` parameter: + +```typst +#plot( + xmin: -3, xmax: 3, ymin: -2, ymax: 2, + style: ( + axis: (stroke: black + 1pt, arrow: "stealth"), + grid: ( + major: (stroke: luma(180) + 0.6pt), + minor: (stroke: luma(220) + 0.3pt), + ), + ticks: ( + length: 0.12, + stroke: black + 0.6pt, + label-size: 0.7em, + ), + ), + ... +) +``` + +== Default Style Values + +#table( + columns: (1.5fr, 1fr, 2fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Property*], [*Default*], [*Description*], + [`axis.stroke`], [`black + 0.8pt`], [Axis line style], + [`axis.arrow`], [`"stealth"`], [Arrow head style], + [`grid.major.stroke`], [`luma(200) + 0.5pt`], [Major grid line style], + [`grid.minor.stroke`], [`luma(230) + 0.3pt`], [Minor grid line style], + [`ticks.length`], [`0.1`], [Tick mark length (cm)], + [`ticks.stroke`], [`black + 0.6pt`], [Tick mark style], + [`ticks.label-size`], [`0.65em`], [Tick label font size], + [`ticks.label-offset`], [`0.15`], [Distance from tick to label], + [`plot.stroke`], [`blue + 1.2pt`], [Default function stroke], + [`plot.samples`], [`100`], [Default sample count], + [`marker.size`], [`0.12`], [Default marker size], + [`labels.size`], [`0.8em`], [Axis label font size], +) + +#pagebreak() + +// ============================================================================= +// PARAMETER REFERENCE +// ============================================================================= + += Parameter Reference + +== `plot` Function + +*Dimensions and Bounds:* + +#table( + columns: (1.3fr, 0.8fr, 1fr, 2fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Parameter*], [*Type*], [*Default*], [*Description*], + [`width`], [float], [6], [Plot width in cm], + [`height`], [float], [6], [Plot height in cm], + [`scale`], [float], [1], [Scale factor for entire plot], + [`xmin`], [float], [auto], [Minimum x value], + [`xmax`], [float], [auto], [Maximum x value], + [`ymin`], [float], [auto], [Minimum y value], + [`ymax`], [float], [auto], [Maximum y value], +) + +#v(0.5em) +*Axis Configuration:* + +#table( + columns: (1.3fr, 0.8fr, 1fr, 2fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Parameter*], [*Type*], [*Default*], [*Description*], + [`xlabel`], [content], [none], [X-axis label], + [`ylabel`], [content], [none], [Y-axis label], + [`xlabel-pos`], [string/array], ["end"], ["end", "center", or (x, y)], + [`ylabel-pos`], [string/array], ["end"], ["end", "center", or (x, y)], + [`xlabel-anchor`], [string], ["west"], [Anchor for x label], + [`ylabel-anchor`], [string], ["south"], [Anchor for y label], + [`xlabel-offset`], [array], [(0.3, 0)], [X label offset (cm)], + [`ylabel-offset`], [array], [(0, 0.3)], [Y label offset (cm)], + [`axis-x-pos`], [string/float], [0], ["bottom", "center", or y-value], + [`axis-y-pos`], [string/float], [0], ["left", "center", or x-value], + [`axis-x-extend`], [float/array], [(0, 0.5)], [X-axis extension (left, right)], + [`axis-y-extend`], [float/array], [(0, 0.5)], [Y-axis extension (bottom, top)], +) + +#v(0.5em) +*Tick Configuration:* + +#table( + columns: (1.3fr, 0.8fr, 1fr, 2fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Parameter*], [*Type*], [*Default*], [*Description*], + [`xtick`], [array/none], [auto], [Custom x tick positions], + [`ytick`], [array/none], [auto], [Custom y tick positions], + [`xtick-step`], [float], [1], [X tick spacing], + [`ytick-step`], [float], [1], [Y tick spacing], + [`xtick-labels`], [array/none], [auto], [Custom x tick labels], + [`ytick-labels`], [array/none], [auto], [Custom y tick labels], + [`xtick-label-step`], [int], [1], [Show x label every N ticks], + [`ytick-label-step`], [int], [1], [Show y label every N ticks], + [`show-origin`], [bool], [true], [Show "0" at origin], + [`unit-label-only`], [bool], [false], [Show only "1" on axes], + [`tick-label-size`], [length], [0.65em], [Tick label font size], + [`axis-label-size`], [length], [0.8em], [Axis label font size], +) + +#pagebreak() + +*Grid Configuration:* + +#table( + columns: (1.3fr, 0.8fr, 1fr, 2fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Parameter*], [*Type*], [*Default*], [*Description*], + [`show-grid`], [bool/string], [false], [true, false, "major", "minor", "both"], + [`minor-grid-step`], [int], [5], [Subdivisions per major tick], + [`grid-label-break`], [bool], [true], [White boxes behind labels], +) + +#v(0.5em) +*Styling:* + +#table( + columns: (1.3fr, 0.8fr, 1fr, 2fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Parameter*], [*Type*], [*Default*], [*Description*], + [`style`], [dictionary], [none], [Style overrides], +) + +== Function/Data Specification + +Each plot item is a dictionary with these fields: + +#table( + columns: (1.2fr, 0.8fr, 2.5fr), + stroke: (x: none, y: 0.3pt + luma(85%)), + inset: 6pt, + [*Field*], [*Type*], [*Description*], + [`fn`], [function], [Function to plot: `x => y`], + [`data`], [array], [Data points: `((x1, y1), (x2, y2), ...)`], + [`domain`], [array], [Function domain: `(xmin, xmax)`], + [`samples`], [int], [Number of samples for function], + [`stroke`], [stroke], [Line style], + [`mark`], [string], [Marker type], + [`mark-size`], [float], [Marker size in cm], + [`mark-fill`], [color], [Marker fill color], + [`label`], [content], [Label text], + [`label-pos`], [string], ["above", "below", "left", "right"], + [`label-at`], [float/string], [x-position or "start"/"end"/"center"], + [`label-anchor`], [string], [Text anchor point], +) + +#pagebreak() + +// ============================================================================= +// EXAMPLES +// ============================================================================= + += Complete Examples + +== Trigonometric Functions + +#example-full( + [```typst +#plot( + width: 10, height: 6, + xmin: -2 * calc.pi, xmax: 2 * calc.pi, + ymin: -1.5, ymax: 1.5, + xlabel: $x$, ylabel: $y$, + xtick: (-2*calc.pi, -calc.pi, 0, calc.pi, 2*calc.pi), + xtick-labels: ($-2pi$, $-pi$, $0$, $pi$, $2pi$), + show-grid: "major", + (fn: x => calc.sin(x), stroke: blue + 1.5pt, label: $sin(x)$, label-pos: 1), + (fn: x => calc.cos(x), stroke: red + 1.5pt, label: $cos(x)$, label-pos: 0), +) + ```], + [ + #plot( + width: 10, height: 6, + xmin: -2 * calc.pi, xmax: 2 * calc.pi, + ymin: -1.5, ymax: 1.5, + xlabel: $x$, ylabel: $y$, + xtick: (-2*calc.pi, -calc.pi, 0, calc.pi, 2*calc.pi), + xtick-labels: ($-2pi$, $-pi$, $0$, $pi$, $2pi$), + show-grid: "major", + (fn: x => calc.sin(x), stroke: blue + 1.5pt, label: $sin(x)$, label-pos: 1), + (fn: x => calc.cos(x), stroke: red + 1.5pt, label: $cos(x)$, label-pos: 0), + ) + ] +) + +== Polynomial with Fine Grid + +#example-full( + [```typst +#plot( + width: 10, height: 8, + xmin: -3, xmax: 3, ymin: -5, ymax: 10, + xlabel: $x$, ylabel: $y$, + show-grid: "both", + minor-grid-step: 5, + ( + fn: x => x * x * x - 3 * x + 1, + stroke: blue + 1.5pt, + label: $f(x) = x^3 - 3x + 1$, + label-side: "above", + label-pos: 0.85, + ), +) + ```], + [ + #plot( + width: 10, height: 8, + xmin: -3, xmax: 3, ymin: -5, ymax: 10, + xlabel: $x$, ylabel: $y$, + show-grid: "both", + minor-grid-step: 5, + ( + fn: x => x * x * x - 3 * x + 1, + stroke: blue + 1.5pt, + label: $f(x) = x^3 - 3x + 1$, + label-side: "above", + label-pos: 0.85, + ), + ) + ] +) + +#pagebreak() + +== Minimal Style Plot + +#example-full( + [```typst +#plot( + width: 10, height: 8, + xmin: -4, xmax: 4, ymin: -2, ymax: 6, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + unit-label-only: true, + show-origin: false, + (fn: x => x * x, stroke: blue + 1.5pt), + (fn: x => -x * x + 5, stroke: red + 1.5pt), +) + ```], + [ + #plot( + width: 10, height: 8, + xmin: -4, xmax: 4, ymin: -2, ymax: 6, + xlabel: $x$, ylabel: $y$, + show-grid: "major", + unit-label-only: true, + show-origin: false, + (fn: x => x * x, stroke: blue + 1.5pt), + (fn: x => -x * x + 5, stroke: red + 1.5pt), + ) + ] +) + +== Data with Trend Line + +#example-full( + [```typst +#plot( + width: 10, height: 7, + xmin: 0, xmax: 6, ymin: 0, ymax: 12, + xlabel: "Time (s)", ylabel: "Distance (m)", + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "both", + minor-grid-step: 5, + (fn: x => 2 * x, stroke: gray + 1pt, domain: (0, 6)), // Trend line + ( + data: ((0.5, 1.2), (1, 2.3), (2, 3.8), (3, 6.2), (4, 7.9), (5, 10.1)), + mark: "o", + mark-size: 0.12, + stroke: none, + ), +) + ```], + [ + #plot( + width: 10, height: 7, + xmin: 0, xmax: 6, ymin: 0, ymax: 12, + xlabel: "Time (s)", ylabel: "Distance (m)", + axis-x-pos: "bottom", axis-y-pos: "left", + show-grid: "both", + minor-grid-step: 5, + (fn: x => 2 * x, stroke: gray + 1pt, domain: (0, 6)), + ( + data: ((0.5, 1.2), (1, 2.3), (2, 3.8), (3, 6.2), (4, 7.9), (5, 10.1)), + mark: "o", + mark-size: 0.12, + stroke: none, + ), + ) + ] +) diff --git a/packages/preview/simple-plot/0.2.6/gallery/data-fit.png b/packages/preview/simple-plot/0.2.6/gallery/data-fit.png new file mode 100644 index 0000000000..3175f5e3ce Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/gallery/data-fit.png differ diff --git a/packages/preview/simple-plot/0.2.6/gallery/data-fit.typ b/packages/preview/simple-plot/0.2.6/gallery/data-fit.typ new file mode 100644 index 0000000000..e00d183a21 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/gallery/data-fit.typ @@ -0,0 +1,25 @@ +#import "@preview/simple-plot:0.2.6": plot, line-plot + +#set page(width: auto, height: auto, margin: 0.5cm) + +// Showcases: data points with model fit, grid-label-break +#plot( + xmin: 0, xmax: 10, + ymin: 0, ymax: 12, + xlabel: [Time (s)], + ylabel: [Distance (m)], + show-grid: "both", + minor-grid-step: 5, + axis-x-pos: "bottom", + axis-y-pos: "left", + // Experimental data points + line-plot( + ((0, 0), (1, 0.5), (2, 1.8), (3, 4.2), (4, 5.1), (5, 6.8), (6, 8.2), (7, 9.5)), + stroke: blue + 1pt, + mark: "*", + mark-fill: blue, + ), + // Theoretical model fit + (fn: x => 0.15 * calc.pow(x, 2) + 0.3 * x, + stroke: red + 1.2pt, label: [Model], label-pos: 0.85, label-side: "below-left"), +) diff --git a/packages/preview/simple-plot/0.2.6/gallery/exponential.png b/packages/preview/simple-plot/0.2.6/gallery/exponential.png new file mode 100644 index 0000000000..e63c384b72 Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/gallery/exponential.png differ diff --git a/packages/preview/simple-plot/0.2.6/gallery/exponential.typ b/packages/preview/simple-plot/0.2.6/gallery/exponential.typ new file mode 100644 index 0000000000..e3acf3474a --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/gallery/exponential.typ @@ -0,0 +1,16 @@ +#import "@preview/simple-plot:0.2.6": plot + +#set page(width: auto, height: auto, margin: 0.5cm) + +// Showcases: domain restriction, grid-label-break +#plot( + xmin: -2, xmax: 3, + ymin: -2, ymax: 5, + xlabel: $x$, + ylabel: $y$, + show-grid: "both", + minor-grid-step: 5, + (fn: x => calc.exp(x), stroke: green + 1.5pt, label: $e^x$, label-pos: 0.3, label-side: "below-left"), + (fn: x => if x > 0 { calc.ln(x) } else { float.nan }, + domain: (0.01, 3), stroke: orange + 1.5pt, label: $ln(x)$, label-pos: 0.9, label-side: "above-right"), +) diff --git a/packages/preview/simple-plot/0.2.6/gallery/extended-axes.png b/packages/preview/simple-plot/0.2.6/gallery/extended-axes.png new file mode 100644 index 0000000000..51368267cb Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/gallery/extended-axes.png differ diff --git a/packages/preview/simple-plot/0.2.6/gallery/extended-axes.typ b/packages/preview/simple-plot/0.2.6/gallery/extended-axes.typ new file mode 100644 index 0000000000..18de766121 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/gallery/extended-axes.typ @@ -0,0 +1,14 @@ +#import "@preview/simple-plot:0.2.6": plot + +#set page(width: auto, height: auto, margin: 0.5cm) + +// Showcases: default axis extension (0.5 on arrow side), grid-label-break +#plot( + xmin: -5, xmax: 5, + ymin: -3, ymax: 5, + xlabel: $x$, + ylabel: $y$, + show-grid: "major", + (fn: x => 0.2 * calc.pow(x, 2) - 2.0, stroke: blue + 1.5pt, label: $f$, label-pos: 0.9, label-side: "below-right"), + (fn: x => -0.5 * x + 1.0, stroke: red + 1.5pt, label: $g$, label-pos: 0.2, label-side: "above-right"), +) diff --git a/packages/preview/simple-plot/0.2.6/gallery/markers.png b/packages/preview/simple-plot/0.2.6/gallery/markers.png new file mode 100644 index 0000000000..e711368217 Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/gallery/markers.png differ diff --git a/packages/preview/simple-plot/0.2.6/gallery/markers.typ b/packages/preview/simple-plot/0.2.6/gallery/markers.typ new file mode 100644 index 0000000000..359d1c6545 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/gallery/markers.typ @@ -0,0 +1,25 @@ +#import "@preview/simple-plot:0.2.6": plot, scatter + +#set page(width: auto, height: auto, margin: 0.5cm) + +// Showcase different marker types with grid-label-break +#plot( + xmin: 0, xmax: 8, + ymin: 0, ymax: 8, + width: 8, height: 8, + show-grid: "major", + axis-x-pos: "bottom", + axis-y-pos: "left", + scatter(((1, 7),), mark: "o", mark-size: 0.2, mark-stroke: blue + 1pt), + scatter(((2, 7),), mark: "*", mark-size: 0.2, mark-fill: blue), + scatter(((3, 7),), mark: "square", mark-size: 0.2, mark-stroke: red + 1pt), + scatter(((4, 7),), mark: "square*", mark-size: 0.2, mark-fill: red), + scatter(((5, 7),), mark: "triangle", mark-size: 0.2, mark-stroke: green + 1pt), + scatter(((6, 7),), mark: "triangle*", mark-size: 0.2, mark-fill: green), + scatter(((1, 5),), mark: "diamond", mark-size: 0.2, mark-stroke: purple + 1pt), + scatter(((2, 5),), mark: "diamond*", mark-size: 0.2, mark-fill: purple), + scatter(((3, 5),), mark: "star", mark-size: 0.2, mark-stroke: orange + 1pt), + scatter(((4, 5),), mark: "star*", mark-size: 0.2, mark-fill: orange), + scatter(((5, 5),), mark: "+", mark-size: 0.2, mark-stroke: black + 1.5pt), + scatter(((6, 5),), mark: "x", mark-size: 0.2, mark-stroke: black + 1.5pt), +) diff --git a/packages/preview/simple-plot/0.2.6/gallery/parabola.png b/packages/preview/simple-plot/0.2.6/gallery/parabola.png new file mode 100644 index 0000000000..52be371765 Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/gallery/parabola.png differ diff --git a/packages/preview/simple-plot/0.2.6/gallery/parabola.typ b/packages/preview/simple-plot/0.2.6/gallery/parabola.typ new file mode 100644 index 0000000000..ac3f6db528 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/gallery/parabola.typ @@ -0,0 +1,13 @@ +#import "@preview/simple-plot:0.2.6": plot + +#set page(width: auto, height: auto, margin: 0.5cm) + +// Showcases: major grid only, grid-label-break, axis extension, integer ticks +#plot( + xmin: -3, xmax: 3, + ymin: -1, ymax: 9, + xlabel: $x$, + ylabel: $y$, + show-grid: "major", + (fn: x => calc.pow(x, 2), stroke: blue + 1.5pt, label: $x^2$, label-pos: 0.7, label-side: "below-right"), +) diff --git a/packages/preview/simple-plot/0.2.6/gallery/scatter.png b/packages/preview/simple-plot/0.2.6/gallery/scatter.png new file mode 100644 index 0000000000..a677df9d6a Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/gallery/scatter.png differ diff --git a/packages/preview/simple-plot/0.2.6/gallery/scatter.typ b/packages/preview/simple-plot/0.2.6/gallery/scatter.typ new file mode 100644 index 0000000000..a1ad63a040 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/gallery/scatter.typ @@ -0,0 +1,21 @@ +#import "@preview/simple-plot:0.2.6": plot, scatter + +#set page(width: auto, height: auto, margin: 0.5cm) + +// Showcases: scatter plots with axes at bottom/left +#plot( + xmin: 0, xmax: 10, + ymin: 0, ymax: 10, + xlabel: $x$, + ylabel: $y$, + show-grid: "both", + minor-grid-step: 5, + axis-x-pos: "bottom", + axis-y-pos: "left", + scatter( + ((1, 2), (2, 3.5), (3, 2.8), (4, 5.2), (5, 4.8), (6, 6.1), (7, 5.9), (8, 7.2), (9, 8.1)), + mark: "*", + mark-fill: blue, + mark-size: 0.15, + ), +) diff --git a/packages/preview/simple-plot/0.2.6/gallery/trig-functions.png b/packages/preview/simple-plot/0.2.6/gallery/trig-functions.png new file mode 100644 index 0000000000..6cb1f21021 Binary files /dev/null and b/packages/preview/simple-plot/0.2.6/gallery/trig-functions.png differ diff --git a/packages/preview/simple-plot/0.2.6/gallery/trig-functions.typ b/packages/preview/simple-plot/0.2.6/gallery/trig-functions.typ new file mode 100644 index 0000000000..90cfd14499 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/gallery/trig-functions.typ @@ -0,0 +1,18 @@ +#import "@preview/simple-plot:0.2.6": plot + +#set page(width: auto, height: auto, margin: 0.5cm) + +// Showcases: custom tick labels, grid-label-break with pi notation +#plot( + xmin: -2.0 * calc.pi, xmax: 2.0 * calc.pi, + ymin: -1.5, ymax: 1.5, + width: 10, height: 5, + xlabel: $x$, + ylabel: $y$, + show-grid: "major", + show-origin: false, // Avoid duplicate "0" with custom xtick-labels + xtick: (-2.0*calc.pi, -calc.pi, calc.pi, 2.0*calc.pi), + xtick-labels: ($-2 pi$, $-pi$, $pi$, $2 pi$), + (fn: x => calc.sin(x), stroke: blue + 1.2pt, samples: 200, label: $sin(x)$, label-pos: 0.65, label-side: "below"), + (fn: x => calc.cos(x), stroke: red + 1.2pt, samples: 200, label: $cos(x)$, label-pos: 0.9, label-side: "above"), +) diff --git a/packages/preview/simple-plot/0.2.6/lib.typ b/packages/preview/simple-plot/0.2.6/lib.typ new file mode 100644 index 0000000000..886a3238e6 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/lib.typ @@ -0,0 +1,921 @@ +// simple-plot - A simple pgfplots-like function plotting library for Typst +// https://github.com/nathan/simple-plot +// License: MIT + +#import "@preview/cetz:0.4.2" as cetz + +// ============================================================================ +// GLOBAL DEFAULTS +// ============================================================================ + +#let _plot-defaults = state("simple-plot-defaults", (:)) + +/// Set default values for all subsequent plots. +/// +/// Example: +/// ```typst +/// #set-plot-defaults(width: 10, height: 8, show-grid: true) +/// ``` +#let set-plot-defaults(..args) = { + _plot-defaults.update(current => { + let new = current + for (key, value) in args.named() { + new.insert(key, value) + } + new + }) +} + +/// Reset all defaults to initial values. +#let reset-plot-defaults() = { + _plot-defaults.update(_ => (:)) +} + +// ============================================================================ +// MARKER DEFINITIONS +// ============================================================================ + +/// Available marker types for scatter plots and data points. +#let marker-types = ( + "o", // circle (hollow) + "*", // circle (filled) + "square", // square (hollow) + "square*", // square (filled) + "triangle", // triangle (hollow) + "triangle*",// triangle (filled) + "diamond", // diamond (hollow) + "diamond*", // diamond (filled) + "star", // star (hollow) + "star*", // star (filled) + "+", // plus + "x", // cross + "|", // vertical bar + "-", // horizontal bar + "none", // no marker +) + +// ============================================================================ +// DEFAULT STYLES +// ============================================================================ + +#let default-style = ( + background: ( + fill: none, + stroke: none, + ), + axis: ( + stroke: black + 0.8pt, + arrow: "stealth", + ), + grid: ( + // Elegant thin grid lines inspired by tkz-fct + major: (stroke: luma(200) + 0.5pt), + minor: (stroke: luma(230) + 0.3pt), + ), + ticks: ( + length: 0.1, + stroke: black + 0.6pt, + label-offset: 0.15, + label-size: 10pt, + ), + plot: ( + stroke: blue + 1.2pt, + samples: 100, + ), + marker: ( + size: 0.12, + stroke: black + 0.8pt, + fill: black, + ), + labels: ( + size: 10pt, + offset: 0.3, + ), + xlabel-style: ( + anchor: "west", + offset: (0.3, 0), + ), + ylabel-style: ( + anchor: "south", + offset: (0, 0.3), + ), +) + +#let merge-styles(user-style) = { + let result = default-style + if user-style != none { + for (key, value) in user-style { + if key in result and type(value) == dictionary { + for (k, v) in value { + result.at(key).insert(k, v) + } + } else { + result.insert(key, value) + } + } + } + result +} + +// ============================================================================ +// UTILITY FUNCTIONS +// ============================================================================ + +// Clip a line segment to a rectangle (all 4 edges) using Liang-Barsky +#let clip-segment(p1, p2, xmin, ymin, xmax, ymax) = { + let (x1, y1) = p1 + let (x2, y2) = p2 + + let dx = x2 - x1 + let dy = y2 - y1 + + let t0 = 0.0 + let t1 = 1.0 + + // Check each edge: left, right, bottom, top + let edges = ( + (-dx, x1 - xmin), + (dx, xmax - x1), + (-dy, y1 - ymin), + (dy, ymax - y1), + ) + + for (p, q) in edges { + if p == 0 { + if q < 0 { return none } + } else { + let t = q / p + if p < 0 { + t0 = calc.max(t0, t) + } else { + t1 = calc.min(t1, t) + } + if t0 > t1 { return none } + } + } + + let nx1 = x1 + t0 * dx + let ny1 = y1 + t0 * dy + let nx2 = x1 + t1 * dx + let ny2 = y1 + t1 * dy + + ((nx1, ny1), (nx2, ny2)) +} + +// Convert user-friendly label-side to CeTZ anchor +// "above" means label is above the point, so anchor at "south" (bottom of text) +#let side-to-anchor(side) = { + if side == none { return none } + let mapping = ( + "above": "south", + "below": "north", + "left": "east", + "right": "west", + "above-left": "south-east", + "above-right": "south-west", + "below-left": "north-east", + "below-right": "north-west", + ) + mapping.at(side, default: side) // fallback to raw anchor if not in mapping +} + +#let format-number(n, precision: 2) = { + if calc.abs(n - calc.round(n)) < 0.0001 { + str(int(calc.round(n))) + } else { + let rounded = calc.round(n * calc.pow(10, precision)) / calc.pow(10, precision) + str(rounded) + } +} + +// Generate ticks with step=1 by default, starting on integers +#let generate-ticks(min, max, step: auto, count: auto) = { + let actual-step = if step != auto { + step + } else if count != auto { + (max - min) / count + } else { + // Default to step=1 for clean integer ticks + 1 + } + + let ticks = () + // Start at the first integer >= min (aligned to step) + let start = calc.ceil(min / actual-step) * actual-step + let pos = start + while pos <= max + 0.0001 { + ticks.push(pos) + pos += actual-step + } + (ticks: ticks, step: actual-step) +} + +// ============================================================================ +// MARKER DRAWING +// ============================================================================ + +#let draw-marker(ctx, pos, marker-type, size, fill-color, stroke-style) = { + import cetz.draw: * + + let (cx, cy) = pos + let s = size + let half = s / 2 + + if marker-type == "o" { + circle((cx, cy), radius: half, stroke: stroke-style, fill: none) + } else if marker-type == "*" { + circle((cx, cy), radius: half, stroke: stroke-style, fill: fill-color) + } else if marker-type == "square" { + rect((cx - half, cy - half), (cx + half, cy + half), stroke: stroke-style, fill: none) + } else if marker-type == "square*" { + rect((cx - half, cy - half), (cx + half, cy + half), stroke: stroke-style, fill: fill-color) + } else if marker-type == "triangle" { + let h = s * 0.866 + line((cx, cy + h/2), (cx - half, cy - h/2), (cx + half, cy - h/2), + close: true, stroke: stroke-style, fill: none) + } else if marker-type == "triangle*" { + let h = s * 0.866 + line((cx, cy + h/2), (cx - half, cy - h/2), (cx + half, cy - h/2), + close: true, stroke: stroke-style, fill: fill-color) + } else if marker-type == "diamond" { + line((cx, cy + half), (cx - half, cy), (cx, cy - half), (cx + half, cy), + close: true, stroke: stroke-style, fill: none) + } else if marker-type == "diamond*" { + line((cx, cy + half), (cx - half, cy), (cx, cy - half), (cx + half, cy), + close: true, stroke: stroke-style, fill: fill-color) + } else if marker-type == "star" or marker-type == "star*" { + let outer = half + let inner = half * 0.4 + let points = () + for i in range(10) { + let angle = calc.pi / 2 + i * calc.pi / 5 + let r = if calc.rem(i, 2) == 0 { outer } else { inner } + points.push((cx + r * calc.cos(angle), cy + r * calc.sin(angle))) + } + line(..points, close: true, stroke: stroke-style, + fill: if marker-type == "star*" { fill-color } else { none }) + } else if marker-type == "+" { + line((cx - half, cy), (cx + half, cy), stroke: stroke-style) + line((cx, cy - half), (cx, cy + half), stroke: stroke-style) + } else if marker-type == "x" { + line((cx - half, cy - half), (cx + half, cy + half), stroke: stroke-style) + line((cx - half, cy + half), (cx + half, cy - half), stroke: stroke-style) + } else if marker-type == "|" { + line((cx, cy - half), (cx, cy + half), stroke: stroke-style) + } else if marker-type == "-" { + line((cx - half, cy), (cx + half, cy), stroke: stroke-style) + } +} + +// ============================================================================ +// MAIN PLOT FUNCTION +// ============================================================================ + +/// Create a 2D plot with axes, grid, and function/data visualization. +/// +/// - xmin (auto, float): Minimum x value +/// - xmax (auto, float): Maximum x value +/// - ymin (auto, float): Minimum y value +/// - ymax (auto, float): Maximum y value +/// - width (auto, float): Plot width in cm +/// - height (auto, float): Plot height in cm +/// - scale (auto, float): Scale factor for the entire plot (default: 1) +/// - xlabel (auto, content): X-axis label +/// - ylabel (auto, content): Y-axis label +/// - xlabel-pos (auto, str, array): X label position ("end", "center", or (x,y)) +/// - ylabel-pos (auto, str, array): Y label position ("end", "center", or (x,y)) +/// - xlabel-anchor (auto, str): X label anchor point +/// - ylabel-anchor (auto, str): Y label anchor point +/// - xlabel-offset (auto, array): X label offset (x, y) in cm +/// - ylabel-offset (auto, array): Y label offset (x, y) in cm +/// - xtick (auto, none, array): X tick positions +/// - ytick (auto, none, array): Y tick positions +/// - xtick-step (auto, float): X tick step (default: 1) +/// - ytick-step (auto, float): Y tick step (default: 1) +/// - xtick-labels (auto, none, array): Custom X tick labels (none = no labels) +/// - ytick-labels (auto, none, array): Custom Y tick labels (none = no labels) +/// - xtick-label-step (auto, int): Show X tick label every N ticks (e.g., 5 = labels at 0,5,10...) +/// - ytick-label-step (auto, int): Show Y tick label every N ticks (e.g., 5 = labels at 0,5,10...) +/// - show-grid (auto, bool, str): Grid display ("major", "minor", "both", true, false) +/// - minor-grid-step (auto, int): Minor grid subdivisions per major tick (default: 5) +/// - grid-label-break (auto, bool): Break grid lines around tick labels (default: true) +/// - unit-label-only (auto, bool): Show only "1" label on axes (not -1), useful for minimal style (default: false) +/// - axis-x-pos (auto, float, str): X-axis y-position ("bottom", "center", or value) +/// - axis-y-pos (auto, float, str): Y-axis x-position ("left", "center", or value) +/// - axis-x-extend (auto, float, array): X-axis extension beyond plot (value or (left, right)) +/// - axis-y-extend (auto, float, array): Y-axis extension beyond plot (value or (bottom, top)) +/// - show-origin (auto, bool): Show "0" label at origin (default: true) +/// - tick-label-size (auto, length): Font size for tick labels (default: 0.65em) +/// - axis-label-size (auto, length): Font size for axis labels x/y (default: 0.8em) +/// - style (none, dictionary): Style overrides +/// - ..functions: Function/data specifications to plot +#let plot( + xmin: auto, + xmax: auto, + ymin: auto, + ymax: auto, + width: auto, + height: auto, + scale: auto, + xlabel: auto, + ylabel: auto, + xlabel-pos: auto, + ylabel-pos: auto, + xlabel-anchor: auto, + ylabel-anchor: auto, + xlabel-offset: auto, + ylabel-offset: auto, + xtick: auto, + ytick: auto, + xtick-step: auto, + ytick-step: auto, + xtick-labels: auto, + ytick-labels: auto, + xtick-label-step: auto, + ytick-label-step: auto, + show-grid: auto, + minor-grid-step: auto, + grid-label-break: auto, + unit-label-only: auto, + axis-x-pos: auto, + axis-y-pos: auto, + axis-x-extend: auto, + axis-y-extend: auto, + show-origin: auto, + tick-label-size: auto, + axis-label-size: auto, + style: none, + ..functions, +) = context { + let defaults = _plot-defaults.get() + + let resolve(val, key, fallback) = { + if val != auto { val } + else if key in defaults { defaults.at(key) } + else { fallback } + } + + let xmin = resolve(xmin, "xmin", -5) + let xmax = resolve(xmax, "xmax", 5) + let ymin = resolve(ymin, "ymin", -5) + let ymax = resolve(ymax, "ymax", 5) + let width = resolve(width, "width", 6) + let height = resolve(height, "height", 6) + let scale = resolve(scale, "scale", 1) + let width = width * scale + let height = height * scale + let xlabel = resolve(xlabel, "xlabel", none) + let ylabel = resolve(ylabel, "ylabel", none) + let xlabel-pos = resolve(xlabel-pos, "xlabel-pos", "end") + let ylabel-pos = resolve(ylabel-pos, "ylabel-pos", "end") + let xlabel-anchor = resolve(xlabel-anchor, "xlabel-anchor", "west") + let ylabel-anchor = resolve(ylabel-anchor, "ylabel-anchor", "south") + let xlabel-offset = resolve(xlabel-offset, "xlabel-offset", (0.3, 0)) + let ylabel-offset = resolve(ylabel-offset, "ylabel-offset", (0, 0.3)) + let xtick = resolve(xtick, "xtick", auto) + let ytick = resolve(ytick, "ytick", auto) + let xtick-step = resolve(xtick-step, "xtick-step", auto) + let ytick-step = resolve(ytick-step, "ytick-step", auto) + let xtick-labels = resolve(xtick-labels, "xtick-labels", auto) + let ytick-labels = resolve(ytick-labels, "ytick-labels", auto) + let xtick-label-step = resolve(xtick-label-step, "xtick-label-step", 1) + let ytick-label-step = resolve(ytick-label-step, "ytick-label-step", 1) + let show-grid = resolve(show-grid, "show-grid", false) + let minor-grid-step = resolve(minor-grid-step, "minor-grid-step", 5) + let grid-label-break = resolve(grid-label-break, "grid-label-break", true) + let unit-label-only = resolve(unit-label-only, "unit-label-only", false) + let axis-x-pos = resolve(axis-x-pos, "axis-x-pos", 0) + let axis-y-pos = resolve(axis-y-pos, "axis-y-pos", 0) + let axis-x-extend = resolve(axis-x-extend, "axis-x-extend", (0, 0.5)) + let axis-y-extend = resolve(axis-y-extend, "axis-y-extend", (0, 0.5)) + let show-origin = resolve(show-origin, "show-origin", true) + let tick-label-size = resolve(tick-label-size, "tick-label-size", auto) + let axis-label-size = resolve(axis-label-size, "axis-label-size", auto) + + // Normalize extend values to (left/bottom, right/top) tuples + let x-extend = if type(axis-x-extend) == array { axis-x-extend } else { (axis-x-extend, axis-x-extend) } + let y-extend = if type(axis-y-extend) == array { axis-y-extend } else { (axis-y-extend, axis-y-extend) } + + let s = merge-styles(style) + + // Override style values with direct parameters if set + if tick-label-size != auto { + s.ticks.label-size = tick-label-size + } + if axis-label-size != auto { + s.labels.size = axis-label-size + } + + // Scale factors in CeTZ canvas units + let x-scale = width / (xmax - xmin) + let y-scale = height / (ymax - ymin) + + let to-canvas(x, y) = { + ((x - xmin) * x-scale, (y - ymin) * y-scale) + } + + let x-axis-y = if axis-x-pos == "bottom" { ymin } + else if axis-x-pos == "center" { 0 } + else { calc.max(ymin, calc.min(ymax, axis-x-pos)) } + + let y-axis-x = if axis-y-pos == "left" { xmin } + else if axis-y-pos == "center" { 0 } + else { calc.max(xmin, calc.min(xmax, axis-y-pos)) } + + let x-ticks = if xtick == none { (ticks: (), step: 1) } + else if xtick == auto { generate-ticks(xmin, xmax, step: xtick-step) } + else { (ticks: xtick, step: if xtick.len() > 1 { xtick.at(1) - xtick.at(0) } else { 1 }) } + + let y-ticks = if ytick == none { (ticks: (), step: 1) } + else if ytick == auto { generate-ticks(ymin, ymax, step: ytick-step) } + else { (ticks: ytick, step: if ytick.len() > 1 { ytick.at(1) - ytick.at(0) } else { 1 }) } + + cetz.canvas(length: 1cm, { + import cetz.draw: * + + set-style( + mark: (fill: black, scale: 1.5), + stroke: (cap: "round", join: "round"), + content: (padding: 2pt), + ) + + // Background + if s.background.fill != none or s.background.stroke != none { + let (bx1, by1) = to-canvas(xmin, ymin) + let (bx2, by2) = to-canvas(xmax, ymax) + rect((bx1, by1), (bx2, by2), fill: s.background.fill, stroke: s.background.stroke) + } + + // Grid bounds - grid stays within the main plot area (no extension) + // Only the axes extend beyond the grid + let grid-x-start = 0 + let grid-x-end = width + let grid-y-start = 0 + let grid-y-end = height + + // Tick and label dimensions (already unitless floats) + let tick-len = s.ticks.length + let label-offset = s.ticks.label-offset + + // Helper: check if a tick value should have a label displayed + let x-has-label(x) = { + if xtick-labels == none { return false } + if calc.abs(x) < 0.0001 { return false } // 0 handled separately + let label-interval = x-ticks.step * xtick-label-step + let at-interval = calc.abs(calc.rem(x, label-interval)) < 0.0001 or calc.abs(calc.rem(x, label-interval) - label-interval) < 0.0001 + if unit-label-only and calc.abs(x - 1) > 0.0001 { return false } + at-interval + } + + let y-has-label(y) = { + if ytick-labels == none { return false } + if calc.abs(y) < 0.0001 { return false } // 0 handled separately + let label-interval = y-ticks.step * ytick-label-step + let at-interval = calc.abs(calc.rem(y, label-interval)) < 0.0001 or calc.abs(calc.rem(y, label-interval) - label-interval) < 0.0001 + if unit-label-only and calc.abs(y - 1) > 0.0001 { return false } + at-interval + } + + // Minor grid (simple, no breaks) + if show-grid == "minor" or show-grid == "both" or show-grid == true { + let minor-x-step = x-ticks.step / minor-grid-step + let minor-y-step = y-ticks.step / minor-grid-step + let nx = int(calc.ceil((xmax - xmin) / minor-x-step)) + 1 + let ny = int(calc.ceil((ymax - ymin) / minor-y-step)) + 1 + + for i in range(nx) { + let x = xmin + i * minor-x-step + if x <= xmax { + let cx = (x - xmin) * x-scale + line((cx, grid-y-start), (cx, grid-y-end), stroke: s.grid.minor.stroke) + } + } + for i in range(ny) { + let y = ymin + i * minor-y-step + if y <= ymax { + let cy = (y - ymin) * y-scale + line((grid-x-start, cy), (grid-x-end, cy), stroke: s.grid.minor.stroke) + } + } + } + + // Major grid (simple, no breaks) + if show-grid == "major" or show-grid == "both" or show-grid == true { + for x in x-ticks.ticks { + let cx = (x - xmin) * x-scale + line((cx, grid-y-start), (cx, grid-y-end), stroke: s.grid.major.stroke) + } + for y in y-ticks.ticks { + let cy = (y - ymin) * y-scale + line((grid-x-start, cy), (grid-x-end, cy), stroke: s.grid.major.stroke) + } + } + + // White background rectangles for tick labels (to mask grid lines) + // This creates the elegant "break" effect by drawing white boxes behind labels + // Inspired by tkz-fct's clean grid breaks + if grid-label-break and (show-grid == "major" or show-grid == "both" or show-grid == true) { + let y-ax-canvas = (x-axis-y - ymin) * y-scale + let x-ax-canvas = (y-axis-x - xmin) * x-scale + + // Scale box dimensions for grid-label-break. + // Keep a stable 10pt baseline here; converting arbitrary lengths to scalars + // (e.g. 10pt -> 10) is not supported directly in Typst arithmetic. + let scale-factor = 1.0 + + // Box padding around text - scales with font size + let pad-x = 0.07 * scale-factor + let pad-y = 0.05 * scale-factor + + // Character dimensions scaled to actual font size + let char-width = 0.17 * scale-factor + let char-height = 0.25 * scale-factor + let minus-width = 0.10 * scale-factor + + // Helper to calculate text width accounting for minus sign + let calc-text-width(val) = { + let label-text = format-number(val) + if val < 0 { + // Negative: minus sign + digits + minus-width + (label-text.len() - 1) * char-width + } else { + label-text.len() * char-width + } + } + + // White boxes for x-axis tick labels (below axis) + // Labels use anchor "north", so text extends downward from anchor point + for x in x-ticks.ticks { + if x-has-label(x) and calc.abs(x - xmax) > 0.0001 { + let cx = (x - xmin) * x-scale + let text-width = calc-text-width(x) + + // Anchor "north" means: anchor point is at top-center of text + // Text extends downward and equally left/right from anchor + let anchor-x = cx + let anchor-y = y-ax-canvas - tick-len - label-offset + + rect( + (anchor-x - text-width / 2 - pad-x, anchor-y - char-height - pad-y), + (anchor-x + text-width / 2 + pad-x, anchor-y + pad-y), + fill: white, stroke: none + ) + } + } + + // White boxes for y-axis tick labels (left of axis) + // Labels use anchor "east", so text extends leftward from anchor point + for y in y-ticks.ticks { + if y-has-label(y) and calc.abs(y - ymax) > 0.0001 { + let cy = (y - ymin) * y-scale + let text-width = calc-text-width(y) + + // Anchor "east" means: anchor point is at right-center of text + // Text extends leftward and equally up/down from anchor + let anchor-x = x-ax-canvas - tick-len - label-offset + let anchor-y = cy + + rect( + (anchor-x - text-width - pad-x, anchor-y - char-height / 2 - pad-y), + (anchor-x + pad-x, anchor-y + char-height / 2 + pad-y), + fill: white, stroke: none + ) + } + } + + // White box for origin label if shown + if show-origin and calc.abs(x-axis-y) < 0.0001 and calc.abs(y-axis-x) < 0.0001 { + let (ox, oy) = to-canvas(0, 0) + // Origin label "0" uses anchor "north-east" + // Text extends down and left from anchor point + let anchor-x = ox - tick-len - 0.05 + let anchor-y = oy - tick-len - 0.05 + let text-width = char-width // Single "0" + + rect( + (anchor-x - text-width - pad-x, anchor-y - char-height - pad-y), + (anchor-x + pad-x, anchor-y + pad-y), + fill: white, stroke: none + ) + } + } + + // Axes (with optional extension beyond plot area) + let (x1, y-ax) = to-canvas(xmin, x-axis-y) + let (x2, _) = to-canvas(xmax, x-axis-y) + let x1-ext = x1 - x-extend.at(0) * x-scale + let x2-ext = x2 + x-extend.at(1) * x-scale + line((x1-ext, y-ax), (x2-ext, y-ax), stroke: s.axis.stroke, mark: (end: s.axis.arrow)) + + let (x-ax, y1) = to-canvas(y-axis-x, ymin) + let (_, y2) = to-canvas(y-axis-x, ymax) + let y1-ext = y1 - y-extend.at(0) * y-scale + let y2-ext = y2 + y-extend.at(1) * y-scale + line((x-ax, y1-ext), (x-ax, y2-ext), stroke: s.axis.stroke, mark: (end: s.axis.arrow)) + + // Ticks and labels (tick-len already defined above) + for (i, x) in x-ticks.ticks.enumerate() { + // Skip tick at xmax (where arrow is) + if calc.abs(x - xmax) < 0.0001 { continue } + let (cx, cy) = to-canvas(x, x-axis-y) + line((cx, cy - tick-len), (cx, cy + tick-len), stroke: s.ticks.stroke) + // Only show label if x is a multiple of (tick-step * label-step) + let label-interval = x-ticks.step * xtick-label-step + let show-this-label = calc.abs(calc.rem(x, label-interval)) < 0.0001 or calc.abs(calc.rem(x, label-interval) - label-interval) < 0.0001 + // If unit-label-only, only show label for x = 1 (not -1 or other values) + if unit-label-only and calc.abs(x - 1) > 0.0001 { + show-this-label = false + } + // Avoid duplicate "0" when explicit origin label is enabled. + if show-origin and calc.abs(x-axis-y) < 0.0001 and calc.abs(y-axis-x) < 0.0001 and calc.abs(x) < 0.0001 { + show-this-label = false + } + if show-this-label and xtick-labels != none { + let label = if xtick-labels == auto { format-number(x) } + else if i < xtick-labels.len() { xtick-labels.at(i) } + else { "" } + if label != "" and label != "0" { + content((cx, cy - tick-len - label-offset), + text(size: s.ticks.label-size)[#label], anchor: "north") + } + } + } + + for (i, y) in y-ticks.ticks.enumerate() { + // Skip tick at ymax (where arrow is) + if calc.abs(y - ymax) < 0.0001 { continue } + let (cx, cy) = to-canvas(y-axis-x, y) + line((cx - tick-len, cy), (cx + tick-len, cy), stroke: s.ticks.stroke) + // Only show label if y is a multiple of (tick-step * label-step) + let label-interval = y-ticks.step * ytick-label-step + let show-this-label = calc.abs(calc.rem(y, label-interval)) < 0.0001 or calc.abs(calc.rem(y, label-interval) - label-interval) < 0.0001 + // If unit-label-only, only show label for y = 1 (not -1 or other values) + if unit-label-only and calc.abs(y - 1) > 0.0001 { + show-this-label = false + } + // Avoid duplicate "0" when explicit origin label is enabled. + if show-origin and calc.abs(x-axis-y) < 0.0001 and calc.abs(y-axis-x) < 0.0001 and calc.abs(y) < 0.0001 { + show-this-label = false + } + if show-this-label and ytick-labels != none { + let label = if ytick-labels == auto { format-number(y) } + else if i < ytick-labels.len() { ytick-labels.at(i) } + else { "" } + if label != "" and label != "0" { + content((cx - tick-len - label-offset, cy), + text(size: s.ticks.label-size)[#label], anchor: "east") + } + } + } + + // Origin label + if show-origin and calc.abs(x-axis-y) < 0.0001 and calc.abs(y-axis-x) < 0.0001 { + let (ox, oy) = to-canvas(0, 0) + content((ox - tick-len - 0.05, oy - tick-len - 0.05), + text(size: s.ticks.label-size)[0], anchor: "north-east") + } + + // Axis labels - positioned at the extended arrow tips + if xlabel != none { + let (lx, ly) = if xlabel-pos == "end" { + // Position at extended arrow tip + let (base-x, base-y) = to-canvas(xmax, x-axis-y) + (base-x + x-extend.at(1) * x-scale, base-y) + } else if xlabel-pos == "center" { to-canvas((xmin + xmax) / 2, x-axis-y) } + else if type(xlabel-pos) == array { to-canvas(xlabel-pos.at(0), xlabel-pos.at(1)) } + else { to-canvas(xmax, x-axis-y) } + let (ox, oy) = xlabel-offset + content((lx + ox, ly + oy), text(size: s.labels.size)[#xlabel], anchor: xlabel-anchor) + } + + if ylabel != none { + let (lx, ly) = if ylabel-pos == "end" { + // Position at extended arrow tip + let (base-x, base-y) = to-canvas(y-axis-x, ymax) + (base-x, base-y + y-extend.at(1) * y-scale) + } else if ylabel-pos == "center" { to-canvas(y-axis-x, (ymin + ymax) / 2) } + else if type(ylabel-pos) == array { to-canvas(ylabel-pos.at(0), ylabel-pos.at(1)) } + else { to-canvas(y-axis-x, ymax) } + let (ox, oy) = ylabel-offset + content((lx + ox, ly + oy), text(size: s.labels.size)[#ylabel], anchor: ylabel-anchor) + } + + // Extended bounds for clipping area + let x-clip-min = xmin - x-extend.at(0) + let x-clip-max = xmax + x-extend.at(1) + let y-clip-min = ymin - y-extend.at(0) + let y-clip-max = ymax + y-extend.at(1) + + // Sampling bounds (extend further so lines reach clip edges) + let sample-margin = calc.max(xmax - xmin, ymax - ymin) * 0.5 + let x-plot-min = x-clip-min - sample-margin + let x-plot-max = x-clip-max + sample-margin + let y-plot-min = y-clip-min - sample-margin + let y-plot-max = y-clip-max + sample-margin + + // Clip bounds in canvas coordinates + let clip-x1 = grid-x-start + let clip-y1 = grid-y-start + let clip-x2 = grid-x-end + let clip-y2 = grid-y-end + + // Plot functions and data (with manual line clipping) + for func-spec in functions.pos() { + let fn = func-spec.at("fn", default: none) + let data-points = func-spec.at("points", default: none) + let stroke-style = func-spec.at("stroke", default: s.plot.stroke) + let mark-type = func-spec.at("mark", default: "none") + let mark-size = func-spec.at("mark-size", default: s.marker.size) + let mark-fill = func-spec.at("mark-fill", default: s.marker.fill) + let mark-stroke = func-spec.at("mark-stroke", default: s.marker.stroke) + let mark-interval = func-spec.at("mark-interval", default: 1) + let label = func-spec.at("label", default: none) + let points-to-draw = () + + if fn != none { + let domain = func-spec.at("domain", default: none) + let domain-min = if domain == none { x-plot-min } else { domain.at(0) } + let domain-max = if domain == none { x-plot-max } else { domain.at(1) } + let samples = func-spec.at("samples", default: s.plot.samples) + let step = (domain-max - domain-min) / samples + + // Collect all valid points first + let all-points = () + for i in range(samples + 1) { + let x = domain-min + i * step + let y = fn(x) + if y != none and not (y).is-nan() { + let (cx, cy) = to-canvas(x, y) + all-points.push((cx, cy, i)) + // Check if point is inside clip area for markers + if cx >= clip-x1 and cx <= clip-x2 and cy >= clip-y1 and cy <= clip-y2 { + points-to-draw.push((cx, cy, i)) + } + } else { + all-points.push(none) // Mark break in function + } + } + + // Draw clipped line segments between consecutive valid points + for j in range(all-points.len() - 1) { + let pt1 = all-points.at(j) + let pt2 = all-points.at(j + 1) + if pt1 != none and pt2 != none { + let (x1, y1, _) = pt1 + let (x2, y2, _) = pt2 + let clipped = clip-segment((x1, y1), (x2, y2), clip-x1, clip-y1, clip-x2, clip-y2) + if clipped != none { + let (p1, p2) = clipped + line(p1, p2, stroke: stroke-style) + } + } + } + + if label != none { + let label-pos = func-spec.at("label-pos", default: 0.8) + let label-side = func-spec.at("label-side", default: none) + let label-anchor = if label-side != none { side-to-anchor(label-side) } else { func-spec.at("label-anchor", default: "south-west") } + // Use axis range unless a domain is explicitly provided. + let label-domain-min = if domain == none { x-clip-min } else { domain-min } + let label-domain-max = if domain == none { x-clip-max } else { domain-max } + let lx = label-domain-min + (label-domain-max - label-domain-min) * label-pos + let ly = fn(lx) + if ly != none and not (ly).is-nan() and ly >= y-clip-min and ly <= y-clip-max { + let (cx, cy) = to-canvas(lx, ly) + content((cx, cy), label, anchor: label-anchor) + } + } + + } else if data-points != none { + let connect = func-spec.at("connect", default: true) + let canvas-points = () + for (i, pt) in data-points.enumerate() { + let (x, y) = pt + if x >= x-clip-min and x <= x-clip-max and y >= y-clip-min and y <= y-clip-max { + let (cx, cy) = to-canvas(x, y) + canvas-points.push((cx, cy)) + points-to-draw.push((cx, cy, i)) + } + } + if connect and canvas-points.len() > 1 { + line(..canvas-points, stroke: stroke-style) + } + if label != none and canvas-points.len() > 0 { + let label-pos = func-spec.at("label-pos", default: 0.8) + let label-side = func-spec.at("label-side", default: none) + let label-anchor = if label-side != none { side-to-anchor(label-side) } else { func-spec.at("label-anchor", default: "south-west") } + let idx = calc.min(int(canvas-points.len() * label-pos), canvas-points.len() - 1) + let (cx, cy) = canvas-points.at(idx) + content((cx, cy), label, anchor: label-anchor) + } + } + + if mark-type != "none" and points-to-draw.len() > 0 { + for (cx, cy, i) in points-to-draw { + if calc.rem(i, mark-interval) == 0 { + draw-marker(none, (cx, cy), mark-type, mark-size, mark-fill, mark-stroke) + } + } + } + } + }) +} + +// ============================================================================ +// CONVENIENCE FUNCTIONS +// ============================================================================ + +/// Quick single function plot with auto-scaling. +#let plot-fn( + fn, + domain: (-5, 5), + ymin: auto, + ymax: auto, + stroke: blue + 1.2pt, + ..args +) = { + let samples = args.named().at("samples", default: 100) + let (y-min, y-max) = if ymin == auto or ymax == auto { + let ys = () + let step = (domain.at(1) - domain.at(0)) / samples + for i in range(samples + 1) { + let x = domain.at(0) + i * step + let y = fn(x) + if y != none and not (y).is-nan() { ys.push(y) } + } + let min-y = calc.min(..ys) + let max-y = calc.max(..ys) + let padding = (max-y - min-y) * 0.1 + (min-y - padding, max-y + padding) + } else { (ymin, ymax) } + + plot( + xmin: domain.at(0), xmax: domain.at(1), + ymin: if ymin == auto { y-min } else { ymin }, + ymax: if ymax == auto { y-max } else { ymax }, + ..args, + (fn: fn, stroke: stroke, domain: domain), + ) +} + +/// Create a scatter plot specification. +#let scatter( + points, + mark: "*", + mark-size: 0.12, + mark-fill: blue, + mark-stroke: blue + 0.8pt, + connect: false, + stroke: none, + label: none, + label-pos: 0.8, + label-anchor: "south-west", +) = ( + points: points, mark: mark, mark-size: mark-size, + mark-fill: mark-fill, mark-stroke: mark-stroke, + connect: connect, stroke: stroke, label: label, + label-pos: label-pos, label-anchor: label-anchor, +) + +/// Create a line plot with markers specification. +#let line-plot( + points, + stroke: blue + 1.2pt, + mark: "o", + mark-size: 0.1, + mark-fill: white, + mark-stroke: blue + 0.8pt, + label: none, + label-pos: 0.8, + label-anchor: "south-west", +) = ( + points: points, stroke: stroke, mark: mark, + mark-size: mark-size, mark-fill: mark-fill, + mark-stroke: mark-stroke, connect: true, + label: label, label-pos: label-pos, label-anchor: label-anchor, +) + +/// Create a function plot specification with markers. +#let func-plot( + fn, + domain: auto, + stroke: blue + 1.2pt, + samples: 100, + mark: "none", + mark-size: 0.1, + mark-fill: blue, + mark-stroke: blue + 0.8pt, + mark-interval: 10, + label: none, + label-pos: 0.8, + label-anchor: "south-west", +) = { + let spec = ( + fn: fn, stroke: stroke, samples: samples, + mark: mark, mark-size: mark-size, mark-fill: mark-fill, + mark-stroke: mark-stroke, mark-interval: mark-interval, + label: label, label-pos: label-pos, label-anchor: label-anchor, + ) + if domain != auto { spec.insert("domain", domain) } + spec +} diff --git a/packages/preview/simple-plot/0.2.6/typst.toml b/packages/preview/simple-plot/0.2.6/typst.toml new file mode 100644 index 0000000000..1b581b5a36 --- /dev/null +++ b/packages/preview/simple-plot/0.2.6/typst.toml @@ -0,0 +1,13 @@ +[package] +name = "simple-plot" +version = "0.2.6" +entrypoint = "lib.typ" +authors = ["Nathan Scheinmann"] +license = "MIT" +description = "Simple, pgfplots-like function plotting for Typst" +repository = "https://github.com/nathan-ed/typst-package-simple-plot" +keywords = ["plot", "graph", "function", "math", "visualization", "chart", "axis"] +categories = ["visualization"] +disciplines = ["mathematics", "physics", "engineering"] +compiler = "0.11.0" +exclude = ["examples/*", "gallery/*", "*.pdf"]