├── docs ├── Docs.pdf ├── template.typ ├── docs.typ └── typst-doc.typ ├── images ├── bar.png ├── pie.png ├── graph.png ├── overlay.png ├── scatter.png └── histogram.png ├── example ├── Plotting.pdf └── main.typ ├── lib.typ ├── typst.toml ├── LICENSE ├── plotst ├── util │ ├── classify.typ │ └── util.typ ├── axis.typ └── plotting.typ └── readme.md /docs/Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/docs/Docs.pdf -------------------------------------------------------------------------------- /images/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/images/bar.png -------------------------------------------------------------------------------- /images/pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/images/pie.png -------------------------------------------------------------------------------- /images/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/images/graph.png -------------------------------------------------------------------------------- /images/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/images/overlay.png -------------------------------------------------------------------------------- /images/scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/images/scatter.png -------------------------------------------------------------------------------- /example/Plotting.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/example/Plotting.pdf -------------------------------------------------------------------------------- /images/histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chayraaa/typst-plotting/HEAD/images/histogram.png -------------------------------------------------------------------------------- /lib.typ: -------------------------------------------------------------------------------- 1 | #import "/plotst/plotting.typ": plot, overlay, scatter_plot, graph_plot, histogram, pie_chart, bar_chart, radar_chart, box_plot 2 | #import "/plotst/axis.typ": axis 3 | #import "/plotst/util/classify.typ": class, class_generator, classify, compare 4 | -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plotst" 3 | version = "0.2.0" 4 | entrypoint = "lib.typ" 5 | authors = ["Pegacraft", "Gewi413"] 6 | license = "MIT" 7 | description = "A library to draw a variety of graphs and plots to use in your papers" 8 | repository = "https://github.com/Pegacraft/typst-plotting" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pegacraffft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/template.typ: -------------------------------------------------------------------------------- 1 | // The project function defines how your document looks. 2 | // It takes your content and some metadata and formats it. 3 | // Go ahead and customize it to your liking! 4 | #let project( 5 | title: "", 6 | subtitle: "", 7 | abstract: [], 8 | authors: (), 9 | date: none, 10 | body, 11 | ) = { 12 | // Set the document's basic properties. 13 | set document(author: authors, title: title) 14 | set page(numbering: "1", number-align: center) 15 | set text(font: "Linux Libertine", lang: "en") 16 | // show heading.where(level: 1): set heading(numbering: "1") 17 | 18 | v(2em) 19 | 20 | // Title row. 21 | align(center)[ 22 | #block(text(weight: 700, 1.75em, title)) 23 | #block(text(1.0em, subtitle)) 24 | #v(4em, weak: true) 25 | #date 26 | ] 27 | 28 | // Author information. 29 | pad( 30 | top: 0.5em, 31 | x: 2em, 32 | grid( 33 | columns: (1fr,) * calc.min(3, authors.len()), 34 | gutter: 1em, 35 | ..authors.map(author => align(center, strong(author))), 36 | ), 37 | ) 38 | 39 | // Abstract. 40 | pad( 41 | x: 2em, 42 | top: 1em, 43 | bottom: 1.1em, 44 | align(center)[ 45 | #heading( 46 | outlined: false, 47 | numbering: none, 48 | text(0.85em, smallcaps[Abstract]), 49 | ) 50 | #abstract 51 | ], 52 | ) 53 | 54 | // Main body. 55 | set par(justify: true) 56 | v(4em) 57 | 58 | body 59 | } -------------------------------------------------------------------------------- /docs/docs.typ: -------------------------------------------------------------------------------- 1 | #import "template.typ": * 2 | #import "typst-doc.typ": parse-module, show-module 3 | #show link: underline 4 | 5 | #show "(deprecated)": block(box(fill: red, "deprecated", inset: 3pt, radius: 3pt)) 6 | // Take a look at the file `template.typ` in the file panel 7 | // to customize this template and discover how it works. 8 | #show: project.with( 9 | title: "Typst-plotting", 10 | subtitle: "Auto generated documentation", 11 | authors: ( 12 | "Pegacraffft", 13 | "Gewi" 14 | ), 15 | // Insert your abstract after the colon, wrapped in brackets. 16 | // Example: `abstract: [This is my abstract...]` 17 | abstract: [ 18 | *Typst-plotting* is a plotting library for #link("https://typst.app/", [Typst]).\ 19 | It supports drawing the following plots/graphs in a variety of styles. 20 | - Scatter plots 21 | - Line charts 22 | - Histograms 23 | - Bar charts 24 | - Pie charts 25 | - Overlaying plots/charts 26 | More features will be added over time. If you have some feedback, let us know! 27 | ], 28 | date: "17.6.2023", 29 | ) 30 | 31 | // We can apply global styling here to affect the looks 32 | // of the documentation. 33 | #set text(font: "Fira Sans") 34 | #show heading.where(level: 1): it => { 35 | align(center, it) 36 | } 37 | #show heading: set text(size: 1.5em) 38 | #show heading.where(level: 3): it => text(size: 1em, style: "italic", block(it.body)) 39 | 40 | 41 | #{ 42 | let options = (allow-breaking: false) 43 | let modules = ( 44 | "Axes": "/plotst/axis.typ", 45 | "Plots": "/plotst/plotting.typ", 46 | "Classification": "/plotst/util/classify.typ", 47 | ) 48 | //set heading(numbering: "1.1.") 49 | outline(indent: 2em, depth: 2) 50 | align(center)[Docs were created with #link("https://github.com/Mc-Zen/typst-doc")[typst-doc]] 51 | pagebreak() 52 | for (name, path) in modules { 53 | let module = parse-module(path, name:name , ) 54 | show-module(module, first-heading-level: 1, allow-breaking: false) 55 | } 56 | } -------------------------------------------------------------------------------- /plotst/util/classify.typ: -------------------------------------------------------------------------------- 1 | #import "util.typ": * 2 | 3 | //----------------- 4 | //THIS FILE CONTAINS EVERYTHING TO CLASSIFY DATA 5 | //----------------- 6 | 7 | 8 | /// This function is used to compare the data in the classifying process. In most cases you can leave it be. \ 9 | /// If you want a different ordinality, you can overwrite this function. \ \ 10 | /// === Return specification 11 | /// - -1 if `val1 < val2` \ 12 | /// - 1 if `val1 > val2` \ 13 | /// - 0 if `val1 == val2` \ \ 14 | #let compare(val1, val2) = { 15 | return if val1 < val2 {-1} else if val1 > val2 {1} else {0} 16 | } 17 | 18 | /// This is the constructor function for a single `class` used to classify data. \ 19 | /// Right now, this is only used for `histograms`. 20 | /// - lower_lim (integer): The lower limit of the class. (Inclusive) 21 | /// - upper_lim (integer): The upper limit of the class. (Exclusive) 22 | #let class(lower_lim, upper_lim) = { 23 | return ( 24 | lower_lim: lower_lim, 25 | upper_lim: upper_lim, 26 | data: () 27 | ) 28 | } 29 | 30 | /// Generates a number of classes similarly how `axis` fills the `values` parameter on its own. It splits the area from `start` to `end` into the with `amount` specified amount of classes.\ 31 | /// Right now, this is only used for `historams`.\ 32 | /// *Example:* \ 33 | /// ```js let classes = class_generator(10000, 50000, 4)``` \ \ 34 | /// This will result in creating the following classes: `(10000 - 20000, 20000 - 30000, 30000 - 40000, 40000 - 50000, 50000 - 100000)`. \ \ 35 | /// - start (integer): The lower limit of the first generated class. 36 | /// - end (integer): The upper limit of the last generated class. 37 | /// - amount (integer): How many classes should be generated. 38 | #let class_generator(start, end, amount) = { 39 | let step = int((end - start) / amount) 40 | let classes = () 41 | for value in range(start, end, step: step) { 42 | classes.push(class(value, value + step)) 43 | } 44 | return classes 45 | } 46 | 47 | /// Classifies the provided data into the given classes. This has to be done to create a `histogram`. 48 | /// - data (array): The data you want to classify (needs to be comparable by the compare function). It's either an `array` of single values or an `array` of `tuples` looking like this: `(amount, value)`. 49 | /// - classes (array): An array of classes the data should be mapped to (`lower_limit` and `upper_limit` need to be comparable). 50 | /// - compare (function): The method used for comparing. Most of the time this doesn't need to be changed. If you want to use a different compare function, look at the specification for it (_see:_ `compare(val1, val2)`). 51 | #let classify(data, classes, compare: compare) = { 52 | let data = transform_data_full(data) 53 | let classes = if "lower_lim" in classes {(classes,)} else {classes} 54 | for (idx, class) in classes.enumerate() { 55 | for value in data { 56 | if compare(value, class.lower_lim) >= 0 and compare(value, class.upper_lim) <= -1 { 57 | class.data.push(value) 58 | } 59 | } 60 | classes.at(idx) = class 61 | } 62 | return if classes.len() == 1 {classes.at(0)} else {classes} 63 | } 64 | -------------------------------------------------------------------------------- /plotst/util/util.typ: -------------------------------------------------------------------------------- 1 | 2 | // Calculates step size for an axis 3 | // full_dist: the distance of the axis while drawing (most likely width or height) 4 | // axis: the axis 5 | // returns: the step size 6 | #let calc_step_size(full_dist, axis) = { 7 | return full_dist / axis.values.len() / axis.step 8 | } 9 | 10 | // transforms a data list ((amount, value),..) to a list only containing values 11 | #let transform_data_full(data_count) = { 12 | 13 | if not type(data_count.at(0)) == "array" { 14 | return data_count 15 | } 16 | let new_data = () 17 | for (amount, value) in data_count { 18 | for _ in range(amount) { 19 | new_data.push(value) 20 | } 21 | } 22 | return new_data 23 | } 24 | 25 | // transforms a data list (val1, val2, ...) to a list looking like this ((amount, val1),...) 26 | #let transform_data_count(data_full) = { 27 | if type(data_full.at(0)) == "array" { 28 | return data_full 29 | } 30 | 31 | // count class occurrences 32 | let new_data = () 33 | for data in data_full { 34 | let found = false 35 | for (idx, entry) in new_data.enumerate() { 36 | if data == entry.at(1) { 37 | new_data.at(idx).at(0) += 1 38 | found = true 39 | break 40 | } 41 | } 42 | if not found { 43 | new_data.push((1, data)) //? 44 | } 45 | } 46 | return new_data 47 | } 48 | 49 | // converts an integer or 2-long array to a width, height dictionary 50 | #let convert_size(size) = { 51 | if type(size) == "int" { return (width: size, height: size) } 52 | if size.len() == 2 { return (width: size.at(0), height: size.at(1)) } 53 | } 54 | 55 | #let draw_marking(data, markings) = { 56 | if markings == none { return } 57 | if markings == "square" { 58 | markings = square(size: 2pt, fill: black, stroke: none) 59 | } else if markings == "circle" { 60 | markings = circle(radius: 1pt, fill: black, stroke: none) 61 | } else if markings == "cross" { 62 | markings = { 63 | place(line(angle: 45deg, length: 1pt)) 64 | place(line(angle: -45deg, length: 1pt)) 65 | place(line(angle: 135deg, length: 1pt)) 66 | place(line(angle: -135deg, length: 1pt)) 67 | } 68 | } 69 | context { 70 | let (width, height) = measure(markings) 71 | place(dx: data.at(0) - width/2, dy: data.at(1) - height/2, markings) 72 | } 73 | } 74 | 75 | // range that supports float parameters 76 | #let float_range(min, max, step: 1) = { 77 | if type(min) == "float" or type(max) == "float" or type(step) == "float" { 78 | let it = () 79 | it.push(min) 80 | if step < 0 { 81 | while it.last() + step > max { 82 | assert(it.last() + step < it.last(), message: "step size too small to decrease float") 83 | it.push(calc.round(it.last() + step, digits: 2)) 84 | } 85 | } else { 86 | while it.last() + step < max { 87 | assert(it.last() + step > it.last(), message: "step size too small to increase float") 88 | it.push(calc.round(it.last() + step, digits: 2)) 89 | } 90 | } 91 | it 92 | } else { 93 | range(min, max, step: step) 94 | } 95 | } 96 | 97 | /// This function generates `(x, y)` data based on a function to use in other equations. 98 | /// - equation (function): A function that accepts the `x` value of the data and returns the proper `y` value. 99 | /// - start (integer, float): The first `x` value that should be generated. 100 | /// - end (integer, float): The last `x` value that should be generated. 101 | /// - precision (integer): How many lines should be plotted between the `start` and `end` value. The higher the value, the more precise the data will get. 102 | #let function_plotter(equation, start, end, precision: 100) = { 103 | let points = () 104 | for step in float_range(start, end, step: (end - start) / precision) { 105 | points.push((step, equation(step))) 106 | } 107 | points.push((end, equation(end))) 108 | return points 109 | } 110 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # A plotting library for Typst 2 | 3 | A Typst library for drawing graphs and plots. 4 | Made by Gewi413 and Pegacraffft 5 | 6 | ## Currently supported graphs 7 | 8 | - Scatter plots 9 | 10 | - Graph charts 11 | 12 | - Histograms 13 | 14 | - Bar charts 15 | 16 | - Pie charts 17 | 18 | - Overlaying plots/charts 19 | 20 | (more to come) 21 | 22 | ## How to use 23 | 24 | To use the package you can import it through this command `import "@preview/plotst:0.2.0": *` (as soon as the pull request first accepted). 25 | The documentation is found in the `Docs.pdf` in the `docs` folder. It contains all functions necessary to use this library. It also includes a tutorial to create every available plot under their respective render methods. 26 | 27 | If you need some example code, check out `main.typ` in the `examples` folder. It also includes a compiled version of the current `main.typ` 28 | 29 | ## Examples: 30 | 31 | All these images were created using the `main.typ` 32 | 33 | ### Scatter plots 34 | 35 | ```js 36 | // Plot 1: 37 | // The data to be displayed 38 | let gender_data = ( 39 | ("w", 1), ("w", 3), ("w", 5), ("w", 4), ("m", 2), ("m", 2), 40 | ("m", 4), ("m", 6), ("d", 1), ("d", 9), ("d", 5), ("d", 8), 41 | ("d", 3), ("d", 1), (0, 11) 42 | ) 43 | 44 | // Create the axes used for the chart 45 | let y_axis = axis(min: 0, max: 11, step: 1, location: "left", helper_lines: true, invert_markings: false, title: "foo") 46 | let x_axis = axis(values: ("", "m", "w", "d"), location: "bottom", helper_lines: true, invert_markings: false, title: "Gender") 47 | 48 | // Combine the axes and the data and feed it to the plot render function. 49 | let pl = plot(data: gender_data, axes: (x_axis, y_axis)) 50 | scatter_plot(pl, (100%,50%)) 51 | 52 | // Plot 2: 53 | // Same as above 54 | let data = ( 55 | (0, 0), (2, 2), (3, 0), (4, 4), (5, 7), (6, 6), (7, 9), (8, 5), (9, 9), (10, 1) 56 | ) 57 | let x_axis = axis(min: 0, max: 11, step: 2, location: "bottom") 58 | let y_axis = axis(min: 0, max: 11, step: 2, location: "left", helper_lines: false) 59 | let pl = plot(data: data, axes: (x_axis, y_axis)) 60 | scatter_plot(pl, (100%, 25%)) 61 | ``` 62 | 63 | 64 | 65 | ![scatter](./images/scatter.png) 66 | 67 | ### Graph charts 68 | 69 | ```js 70 | // The data to be displayed 71 | let data = ( 72 | (0, 0), (2, 2), (3, 0), (4, 4), (5, 7), (6, 6), (7, 9), (8, 5), (9, 9), (10, 1) 73 | ) 74 | 75 | // Create the axes used for the chart 76 | let x_axis = axis(min: 0, max: 11, step: 2, location: "bottom") 77 | let y_axis = axis(min: 0, max: 11, step: 2, location: "left", helper_lines: false) 78 | 79 | // Combine the axes and the data and feed it to the plot render function. 80 | let pl = plot(data: data, axes: (x_axis, y_axis)) 81 | graph_plot(pl, (100%, 25%)) 82 | graph_plot(pl, (100%, 25%), rounding: 30%, caption: "Graph Plot with caption and rounding") 83 | ``` 84 | 85 | 86 | 87 | ![graph](./images/graph.png) 88 | 89 | ### Histograms 90 | 91 | ```js 92 | 93 | // Plot 1: 94 | // The data to be displayed 95 | let data = ( 96 | 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 97 | 18000, 18000, 28000, 28000, 28000, 28000, 28000, 28000, 98 | 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 99 | 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 100 | 35000, 46000, 75000, 95000 101 | ) 102 | 103 | // Classify the data 104 | let classes = class_generator(10000, 50000, 4) 105 | classes.push(class(50000, 100000)) 106 | classes = classify(data, classes) 107 | 108 | // Create the axes used for the chart 109 | let x_axis = axis(min: 0, max: 100000, step: 10000, location: "bottom") 110 | let y_axis = axis(min: 0, max: 31, step: 5, location: "left", helper_lines: true) 111 | 112 | // Combine the axes and the data and feed it to the plot render function. 113 | let pl = plot(data: classes, axes: (x_axis, y_axis)) 114 | histogram(pl, (100%, 40%), stroke: black, fill: (purple, blue, red, green, yellow)) 115 | 116 | // Plot 2: 117 | // Create the different classes 118 | let classes = () 119 | classes.push(class(11, 13)) 120 | classes.push(class(13, 15)) 121 | classes.push(class(1, 6)) 122 | classes.push(class(6, 11)) 123 | classes.push(class(15, 30)) 124 | 125 | // Define the data to map 126 | let data = ((20, 2), (30, 7), (16, 12), (40, 13), (5, 17)) 127 | 128 | // Create the axes 129 | let x_axis = axis(min: 0, max: 31, step: 1, location: "bottom", show_markings: false) 130 | let y_axis = axis(min: 0, max: 41, step: 5, location: "left", helper_lines: true) 131 | 132 | // Classify the data 133 | classes = classify(data, classes) 134 | 135 | // Combine the axes and the data and feed it to the plot render function. 136 | let pl = plot(axes: (x_axis, y_axis), data: classes) 137 | histogram(pl, (100%, 40%)) 138 | ``` 139 | 140 | 141 | 142 | ![histogram](./images/histogram.png) 143 | 144 | ### Bar charts 145 | 146 | ```js 147 | // Plot 1: 148 | // The data to be displayed 149 | let data = ((10, "Monday"), (5, "Tuesday"), (15, "Wednesday"), (9, "Thursday"), (11, "Friday")) 150 | 151 | // Create the necessary axes 152 | let y_axis = axis(values: ("", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"), location: "left", show_markings: true) 153 | let x_axis = axis(min: 0, max: 20, step: 2, location: "bottom", helper_lines: true) 154 | 155 | // Combine the axes and the data and feed it to the plot render function. 156 | let pl = plot(axes: (x_axis, y_axis), data: data) 157 | bar_chart(pl, (100%, 33%), fill: (purple, blue, red, green, yellow), bar_width: 70%, rotated: true) 158 | 159 | // Plot 2: 160 | // Same as above, but with numbers as data 161 | let data_2 = ((20, 2), (30, 7), (16, 12), (40, 13), (5, 17)) 162 | let y_axis_2 = axis(min: 0, max: 41, step: 5, location: "left", show_markings: true, helper_lines: true) 163 | let x_axis_2 = axis(min: 0, max: 21, step: 1, location: "bottom") 164 | let pl_2 = plot(axes: (x_axis_2, y_axis_2), data: data_2) 165 | bar_chart(pl_2, (100%, 60%), bar_width: 100%) 166 | ``` 167 | 168 | 169 | 170 | ![bar](./images/bar.png) 171 | 172 | ### Pie charts 173 | 174 | ```js 175 | show: r => columns(2, r) 176 | 177 | // create the sample data 178 | let data = ((10, "Male"), (20, "Female"), (15, "Divers"), (2, "Other") 179 | 180 | // Skip the axis step, as no axes are needed 181 | 182 | // Put the data into a plot 183 | let p = plot(data: data) 184 | 185 | // Display the pie_charts in all different display ways 186 | pie_chart(p, (100%, 20%), display_style: "legend-inside-chart") 187 | pie_chart(p, (100%, 20%), display_style: "hor-chart-legend") 188 | pie_chart(p, (100%, 20%), display_style: "hor-legend-chart") 189 | pie_chart(p, (100%, 20%), display_style: "vert-chart-legend") 190 | pie_chart(p, (100%, 20%), display_style: "vert-legend-chart") 191 | ``` 192 | 193 | 194 | 195 | ![pie](./images/pie.png) 196 | 197 | **Overlaid Graphs** 198 | 199 | ```js 200 | // Create the data for the two plots to overlay 201 | let data_scatter = ( 202 | (0, 0), (2, 2), (3, 0), (4, 4), (5, 7), (6, 6), (7, 9), (8, 5), (9, 9), (10, 1) 203 | ) 204 | let data_graph = ( 205 | (0, 3), (1, 5), (2, 1), (3, 7), (4, 3), (5, 5), (6, 7),(7, 4),(11, 6) 206 | ) 207 | 208 | // Create the axes for the overlay plot 209 | let x_axis = axis(min: 0, max: 11, step: 2, location: "bottom") 210 | let y_axis = axis(min: 0, max: 11, step: 2, location: "left", helper_lines: false) 211 | 212 | // create a plot for each individual plot type and save the render call 213 | let pl_scatter = plot(data: data_scatter, axes: (x_axis, y_axis)) 214 | let scatter_display = scatter_plot(pl_scatter, (100%, 25%), stroke: red) 215 | let pl_graph = plot(data: data_graph, axes: (x_axis, y_axis)) 216 | let graph_display = graph_plot(pl_graph, (100%, 25%), stroke: blue) 217 | 218 | // overlay the plots using the overlay function 219 | overlay((scatter_display, graph_display), (100%, 25%)) 220 | 221 | ``` 222 | 223 | 224 | 225 | ![overlay](./images/overlay.png) 226 | -------------------------------------------------------------------------------- /example/main.typ: -------------------------------------------------------------------------------- 1 | #import "/lib.typ": * // For local testing 2 | #import "../plotst/util/util.typ": function_plotter 3 | //#import "@preview/plotst:0.1.0": * 4 | 5 | #let print(desc: "", content) = { 6 | desc 7 | repr(content) 8 | [ \ ] 9 | } 10 | #let scatter_plot_test() = { 11 | 12 | let gender_data = ( 13 | ("w", 1), ("w", 3), ("w", 5), ("w", 4), ("m", 2), ("m", 2), ("m", 4), ("m", 6), ("d", 1), ("d", 9), ("d", 5), ("d", 8), ("d", 3), ("d", 1) 14 | ) 15 | let y_axis = axis(min: 0, max: 11, step: 1, location: "left", helper_lines: true, invert_markings: false, title: "foo", value_formatter: "{}€") 16 | 17 | let y_axis_right = axis(min: 1, max: 11, step: 1, location: "right", helper_lines: false, invert_markings: false, title: "foo", stroke: 7pt + red, show_arrows: false, value_formatter: i => datetime(year: 1984, month: 1, day: i).display("[day].[month].")) 18 | let gender_axis_x = axis(values: ("", "m", "w", "d"), location: "bottom", helper_lines: true, invert_markings: false, title: "Gender", show_arrows: false) 19 | let pl = plot(data: gender_data, axes: (gender_axis_x, y_axis, y_axis_right)) 20 | scatter_plot(pl, (100%,50%)) 21 | let data = ( 22 | (0, 0), (2, 2), (3, 0), (4, 4), (5, 7), (6, 6), (7, 9), (8, 5), (9, 9), (10, 1) 23 | ) 24 | let x_axis = axis(min: 0, max: 11, step: 2, location: "bottom") 25 | let y_axis = axis(min: 0, max: 11, step: 2, location: "left", helper_lines: false, show_values: false) 26 | let pl = plot(data: data, axes: (x_axis, y_axis)) 27 | scatter_plot(pl, (100%, 25%)) 28 | } 29 | 30 | #let graph_plot_test() = { 31 | let data = ( 32 | (0, 4), (2, 2), (3, 0), (4, 4), (5, 7), (6, 6), (7, 9), (8, 5), (9, 9), (10, 1) 33 | ) 34 | let data2 = ( 35 | (0, 0), (2, 2), (3, 1), (4, 4), (5, 2), (6, 6), (7, 5), (8, 7), (9, 10), (10, 3) 36 | ) 37 | let x_axis = axis(min: 0, max: 11, step: 2, location: "bottom") 38 | let y_axis = axis(min: 0, max: 11, step: 2, location: "left", helper_lines: false) 39 | let pl = plot(data: data, axes: (x_axis, y_axis)) 40 | graph_plot(pl, (100%, 25%), markings: []) 41 | graph_plot(pl, (100%, 25%), rounding: 30%, caption: "Graph Plot with caption and rounding", markings: [#emoji.rocket]) 42 | } 43 | 44 | #let histogram_test() = { 45 | let data = ( 46 | 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000,28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 35000, 46000, 75000, 95000 47 | ) 48 | let classes = class_generator(10000, 50000, 4) 49 | classes.push(class(50000, 100000)) 50 | classes = classify(data, classes) 51 | let x_axis = axis(min: 0, max: 100000, step: 10000, location: "bottom") 52 | let y_axis = axis(min: 0, max: 31, step: 5, location: "left", helper_lines: true) 53 | let pl = plot(data: classes, axes: (x_axis, y_axis)) 54 | histogram(pl, (100%, 40%), stroke: black, fill: (purple, blue, red, green, yellow)) 55 | } 56 | 57 | #let histogram_test_2() = { 58 | let classes = () 59 | classes.push(class(11, 13)) 60 | classes.push(class(13, 15)) 61 | classes.push(class(1, 6)) 62 | classes.push(class(6, 11)) 63 | classes.push(class(15, 30)) 64 | 65 | let data = ((20, 2), (30, 7), (16, 12), (40, 13), (5, 17)) 66 | 67 | let x_axis = axis(min: 0, max: 31, step: 1, location: "bottom", show_markings: false) 68 | let y_axis = axis(min: 0, max: 41, step: 5, location: "left", helper_lines: true) 69 | 70 | classes = classify(data, classes) 71 | let pl = plot(axes: (x_axis, y_axis), data: classes) 72 | histogram(pl, (100%, 40%)) 73 | } 74 | 75 | #let pie_chart_test() = { 76 | show: r => columns(2, r) 77 | let data = ((10, "Male"), (20, "Female"), (15, "Divers"), (2, "Other")) 78 | let data2 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 79 | 80 | let p = plot(data: data) 81 | pie_chart(p, (100%, 20%), display_style: "legend-inside-chart") 82 | pie_chart(p, (100%, 20%), display_style: "hor-chart-legend") 83 | pie_chart(p, (100%, 20%), display_style: "hor-legend-chart") 84 | pie_chart(p, (100%, 20%), display_style: "vert-chart-legend") 85 | pie_chart(p, (100%, 20%), display_style: "vert-legend-chart") 86 | } 87 | 88 | #let bar_chart_test() = { 89 | let data = ((10, "Monday"), (5, "Tuesday"), (15, "Wednesday"), (9, "Thursday"), (11, "Friday")) 90 | 91 | let y_axis = axis(values: ("", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"), location: "left", show_markings: true) 92 | let x_axis = axis(min: 0, max: 20, step: 2, location: "bottom", helper_lines: true) 93 | 94 | let pl = plot(axes: (x_axis, y_axis), data: data) 95 | bar_chart(pl, (100%, 33%), fill: (purple, blue, red, green, yellow), bar_width: 70%, rotated: true) 96 | 97 | let data_2 = ((20, 2), (30, 7), (16, 12), (40, 13), (5, 17)) 98 | let y_axis_2 = axis(min: 0, max: 41, step: 5, location: "left", show_markings: true, helper_lines: true) 99 | let x_axis_2 = axis(min: 0, max: 21, step: 1, location: "bottom") 100 | let pl_2 = plot(axes: (x_axis_2, y_axis_2), data: data_2) 101 | bar_chart(pl_2, (100%, 60%), bar_width: 100%) 102 | } 103 | 104 | // TODO 105 | #let overlay_test() = { 106 | let data_scatter = ( 107 | (0, 0), (2, 2), (3, 0), (4, 4), (5, 7), (6, 6), (7, 9), (8, 5), (9, 9), (10, 1) 108 | ) 109 | let data_graph = ( 110 | (0, 3), (1, 5), (2, 1), (3, 7), (4, 3), (5, 5), (6, 7),(7, 4),(11, 6) 111 | ) 112 | let x_axis = axis(min: 0, max: 11, step: 2, location: "bottom") 113 | let y_axis = axis(min: 0, max: 11, step: 2, location: "left", helper_lines: false) 114 | let pl_scatter = plot(data: data_scatter, axes: (x_axis, y_axis)) 115 | let scatter_display = scatter_plot(pl_scatter, (100%, 25%), stroke: red) 116 | let pl_graph = plot(data: data_graph, axes: (x_axis, y_axis)) 117 | let graph_display = graph_plot(pl_graph, (100%, 25%), stroke: blue) 118 | scatter_display 119 | graph_display 120 | overlay((scatter_display, graph_display), (100%, 25%)) 121 | 122 | x_axis = axis(min: 0, max: 11, step: 2, location: "bottom", show_values: false) 123 | y_axis = axis(min: 0, max: 11, step: 2, location: "left", show_values: false) 124 | let ice = (data: ((0,0),(3,3),(0,10)), axes: (x_axis, y_axis)) 125 | let a = graph_plot(ice, (100%, 25%), fill: blue.lighten(50%), markings: none, stroke: none, caption: "foo") 126 | let water = (data: ((0,0),(3,3),(10,7), (10,0)), axes: (x_axis, y_axis)) 127 | let b = graph_plot(water, (100%, 25%), fill: blue, markings: none, stroke: none) 128 | let steam = (data: ((3,3),(10,7),(10,10),(0,10)), axes: (x_axis, y_axis)) 129 | let c = graph_plot(steam, (100%, 25%), fill: yellow, markings: none, stroke: none) 130 | overlay((a, b, c), (50%, 25%)) 131 | } 132 | 133 | #let radar_test() = { 134 | let data = ( 135 | (0,6),(1,7),(2,5),(3,4),(4,4),(5,7),(6,6),(7,6), 136 | ) 137 | let y_axis = axis(min:0, max: 8, location: "left", helper_lines: true) 138 | let x_axis = axis(min:0, max: 8, location: "bottom") 139 | 140 | let pl = plot(data: data, axes: (x_axis, y_axis)) 141 | radar_chart(pl, (100%,60%)) 142 | } 143 | 144 | #let function_test() = { 145 | let data = function_plotter(x => {2*(x*x) + 3*x + 3}, 0, 8.3, precision: 100) 146 | let data2 = function_plotter(x => {1*(x*x) + 3*x + 3}, 0, 11.4, precision: 100) 147 | let x_axis = axis(min: 0, max: 20, step: 1, location: "bottom") 148 | let y_axis = axis(min: 0, max: 151, step: 50, location: "left", helper_lines: true) 149 | let p1 = graph_plot(plot(axes: (x_axis, y_axis), data: data), (100%, 50%), markings: [], stroke: red) 150 | let p2 = graph_plot(plot(axes: (x_axis, y_axis), data: data2), (100%, 50%), markings: [], stroke: green) 151 | overlay((p1, p2), (100%, 50%)) 152 | } 153 | 154 | #let box_plot_test() = { 155 | box_plot(box_width: 70%, pre_calculated: false, plot(axes: ( 156 | axis(values: ("", "(a)", "(b)", "(c)"), location: "bottom", show_markings: false), 157 | axis(min: 0, max: 10, step: 1, location: "left", helper_lines: true), 158 | ), 159 | data:((1, 3, 4, 4, 5, 6, 7, 8), (1, 3, 4, 4, 5, 7, 8), (1, 3, 4, 5, 7)) 160 | ), (100%, 40%), caption: none) 161 | } 162 | 163 | #let cumsum_test() = { 164 | datetime(year: 2023, month: 1, day: 20) - datetime.today() 165 | let data = range(1,31).map(i=> (datetime(year: 2023, month: 1, day: i),2)) 166 | let dates = data.map(it => it.at(0)) 167 | let newdata = () 168 | let sum = 0 169 | for d in data { 170 | sum += d.at(1) 171 | newdata.push((d.at(0).display(), sum)) 172 | } 173 | let _ = newdata.remove(0) 174 | let x_axis = axis(values: dates.map(it=> it.display()), location: "bottom") 175 | let y_axis = axis(min: 0, max: sum, step: 10, location: "left") 176 | graph_plot(plot(axes: (x_axis, y_axis), data: newdata), (100%, 50%)) 177 | } 178 | 179 | #let box_plot_test() = { 180 | box_plot(box_width: 70%, pre_calculated: false, plot(axes: ( 181 | axis(values: ("", "(a)", "(b)", "(c)"), location: "bottom", show_markings: false), 182 | axis(min: -5, max: 100, step: 10, location: "left"), 183 | ), 184 | data:((10, 20, 30, 50, 60), (5, 20, 25, 30, 45), (6, 19, 23, 37, 98)) 185 | ), (100%, 40%), caption: none) 186 | } 187 | 188 | #let paper_test() = { 189 | set par(justify: true) 190 | pagebreak() 191 | [ 192 | #set align(center) 193 | = This is my paper 194 | #set align(left) 195 | #show: r => columns(2, r) 196 | #lorem(100) 197 | == Scatter plots 198 | #lorem(50) 199 | #{ 200 | let data = ( 201 | (0, 0), (1, 2), (2, 4), (3, 6), (4, 8), (5, 3), (6, 6),(7, 9),(11, 12) 202 | ) 203 | let x_axis = axis(min: 0, max: 11, step: 1, location: "bottom") 204 | let y_axis = axis(min: 0, max: 13, step: 2, location: "left", helper_lines: true) 205 | let p = plot(data: data, axes: (x_axis, y_axis)) 206 | scatter_plot(p, (100%, 20%)) 207 | } 208 | == Histograms 209 | #lorem(150) 210 | #{ 211 | let data = ( 212 | 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000,28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 35000, 46000, 75000, 95000 213 | ) 214 | let classes = class_generator(10000, 50000, 4) 215 | classes.push(class(50000, 100000)) 216 | classes = classify(data, classes) 217 | 218 | let x_axis = axis(min: 0, max: 100000, step: 20000, location: "bottom", show_markings: false, title: "Wert x", ) 219 | 220 | let y_axis = axis(min: 0, max: 26, step: 3, location: "left", helper_lines: true, title: "Wert y und anderes Zeug", ) 221 | let pl = plot(data: classes, axes: (x_axis, y_axis)) 222 | histogram(pl, (100%, 20%), stroke: black, fill: gray) 223 | } 224 | 225 | == Pie charts 226 | #{ 227 | lorem(120) 228 | let data = ((10, "Male"), (20, "Female"), (15, "Divers"), (2, "Other")) 229 | let pl = plot(data: data) 230 | pie_chart(pl, (100%, 20%), display_style: "hor-chart-legend") 231 | } 232 | #{ 233 | let data = ((5, "0-18"), (9, "18-30"), (25, "30-60"), (7, "60+")) 234 | let pl = plot(data: data) 235 | pie_chart(pl, (100%, 20%), display_style: "hor-chart-legend") 236 | lorem(200) 237 | } 238 | == Bar charts 239 | #{ 240 | lorem(50) 241 | let data = ((10, "Monday"), (5, "Tuesday"), (15, "Wednesday"), (9, "Thursday"), (11, "Friday")) 242 | 243 | let y_axis = axis(values: ("", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"), location: "left", show_markings: true) 244 | let x_axis = axis(min: 0, max: 20, step: 2, location: "bottom", helper_lines: true, title: "Visitors") 245 | 246 | let pl = plot(axes: (x_axis, y_axis), data: data) 247 | bar_chart(pl, (100%, 140pt), fill: (purple, blue, red, green, yellow), bar_width: 70%, rotated: true) 248 | 249 | let data_2 = ((20, 2), (30, 3), (16, 4), (40, 6), (5, 7)) 250 | let y_axis_2 = axis(min: 0, max: 41, step: 10, location: "left", show_markings: true, helper_lines: true) 251 | let x_axis_2 = axis(min: 0, max: 9, step: 1, location: "bottom") 252 | let pl_2 = plot(axes: (x_axis_2, y_axis_2), data: data_2) 253 | bar_chart(pl_2, (100%, 120pt), fill: (purple, blue, red, green, yellow), bar_width: 70%) 254 | 255 | lorem(95) 256 | } 257 | ] 258 | } 259 | 260 | #{ 261 | scatter_plot_test() 262 | graph_plot_test() 263 | pagebreak() 264 | histogram_test() 265 | histogram_test_2() 266 | pagebreak() 267 | pie_chart_test() 268 | pagebreak() 269 | bar_chart_test() 270 | overlay_test() 271 | radar_test() 272 | function_test() 273 | box_plot_test() 274 | //cumsum_test() 275 | 276 | paper_test() 277 | } 278 | // TODO: 279 | // fix points when choosing rounding in graph plot <- Gewi 280 | // bar chart <- Karla 281 | // - make bars realign on right/top 282 | // box plot <- Karla 283 | // math graph (to display equations) 284 | // graph overlapping. Should make it possible to lay one graph onto another. Usefull when wanting to draw mathematical equations into a scatter plot 285 | // util for mean, median, quartil <- Karla 286 | // titles for axes (partly done) <- Karla, Gewi 287 | // fix axis titles being on wrong side 288 | // xyarea chart gewi //done by fill: red i guess -------------------------------------------------------------------------------- /docs/typst-doc.typ: -------------------------------------------------------------------------------- 1 | // Source code for the typst-doc package 2 | 3 | // Color to highlight function names in 4 | #let fn-color = rgb("#4b69c6") 5 | 6 | // Colors for Typst types 7 | #let type-colors = ( 8 | "content": rgb("#a6ebe6"), 9 | "color": rgb("#a6ebe6"), 10 | "string": rgb("#d1ffe2"), 11 | "none": rgb("#ffcbc4"), 12 | "auto": rgb("#ffcbc4"), 13 | "boolean": rgb("#ffedc1"), 14 | "integer": rgb("#e7d9ff"), 15 | "float": rgb("#e7d9ff"), 16 | "ratio": rgb("#e7d9ff"), 17 | "length": rgb("#e7d9ff"), 18 | "angle": rgb("#e7d9ff"), 19 | "relative-length": rgb("#e7d9ff"), 20 | "fraction": rgb("#e7d9ff"), 21 | "symbol": rgb("#eff0f3"), 22 | "array": rgb("#eff0f3"), 23 | "dictionary": rgb("#eff0f3"), 24 | "arguments": rgb("#eff0f3"), 25 | "selector": rgb("#eff0f3"), 26 | "module": rgb("#eff0f3"), 27 | "stroke": rgb("#eff0f3"), 28 | "function": rgb("#f9dfff"), 29 | ) 30 | 31 | #let get-type-color(type) = type-colors.at(type, default: rgb("#eff0f3")) 32 | 33 | // Create beautiful, colored type box 34 | #let type-box(type) = { 35 | let color = get-type-color(type) 36 | h(2pt) 37 | box(outset: 2pt, fill: color, radius: 2pt, raw(type)) 38 | h(2pt) 39 | } 40 | 41 | // Create a parameter description block, containing name, type, description and optionally the default value. 42 | #let param-description-block(name, types, content, show-default: false, default: none, breakable: false) = block( 43 | inset: 10pt, fill: luma(98%), width: 100%, 44 | breakable: breakable, 45 | [ 46 | #text(weight: "bold", size: 1.1em, name) 47 | #h(.5cm) 48 | #types.map(x => type-box(x)).join([ #text("or",size:.6em) ]) 49 | 50 | #eval("[" + content + "]") 51 | 52 | #if show-default [ Default: #raw(lang: "typc", default) ] 53 | ] 54 | ) 55 | 56 | 57 | 58 | /// Parse an argument list from source code at given position. 59 | /// This function returns `none`, if the argument list is not properly closed. 60 | /// Otherwise, a dictionary is returned with an entry for each parsed 61 | /// argument name. The values are dictionaries that may be empty or 62 | /// have an entry for `default` containing a string with the parsed 63 | /// default value for this argument. 64 | /// 65 | /// 66 | /// 67 | /// *Example* 68 | /// 69 | /// Let's take some source code: 70 | /// ```typ 71 | /// #let func(p1, p2: 3pt, p3: (), p4: (entries: ())) = {...} 72 | /// ``` 73 | /// Here, we would call `parse-argument-list(source-code, 9)` and retrieve 74 | /// #pad(x: 1em, ```typc 75 | /// ( 76 | /// p0: (:), 77 | /// p1: (default: "3pt"), 78 | /// p2: (default: "()"), 79 | /// p4: (default: "(entries: ())"), 80 | /// ) 81 | /// ```) 82 | /// 83 | /// - module-content (string): Source code. 84 | /// - index (integer): Index where the argument list starts. This index should point to the character *next* to the function name, i.e. to the opening brace `(` of the argument list if there is one (note, that function aliases for example produced by `myfunc.where(arg1: 3)` do not have an argument list). 85 | /// -> none, dictionary 86 | #let parse-argument-list(module-content, index) = { 87 | if module-content.at(index) != "(" { return (:) } 88 | index += 1 89 | let brace-level = 1 90 | let arg-strings = () 91 | let current-arg = "" 92 | for c in module-content.slice(index) { 93 | if c == "(" { brace-level += 1 } 94 | if c == ")" { brace-level -= 1 } 95 | if c == "," and brace-level == 1 { 96 | arg-strings.push(current-arg) 97 | current-arg = "" 98 | continue 99 | } 100 | if brace-level == 0 { 101 | arg-strings.push(current-arg) 102 | break 103 | } 104 | current-arg += c 105 | } 106 | if brace-level > 0 { return none } 107 | let args = (:) 108 | for arg in arg-strings { 109 | if arg.trim().len() == 0 { continue } 110 | let colon-pos = arg.position(":") 111 | if colon-pos == none { 112 | args.insert(arg.trim(), (:)) 113 | } else { 114 | let name = arg.slice(0, colon-pos) 115 | let default-value = arg.slice(colon-pos + 1) 116 | args.insert(name.trim(), (default: default-value.trim())) 117 | } 118 | } 119 | return args 120 | } 121 | 122 | // #parse-argument-list("sadsdasd (p0, p1: 3, p2: (), p4: (entries: ())) = ) asd", 9) 123 | 124 | 125 | // Matches Typst docstring for a function declaration. Example: 126 | // 127 | // // This function does something 128 | // // 129 | // // param1 (string): This is param1 130 | // // param2 (content, length): This is param2. 131 | // // Yes, it really is. 132 | // #let something(param1, param2) = { 133 | // 134 | // } 135 | // 136 | // The entire block may be indented by any amount, the declaration can either start with `#let` or `let`. The docstring must start with `///` on every line and the function declaration needs to start exactly at the next line. 137 | // #let docstring-matcher = regex(`((?:[^\S\r\n]*/{3} ?.*\n)+)[^\S\r\n]*#?let (\w[\w\d\-_]+)`.text) 138 | #let docstring-matcher = regex(`([^\S\r\n]*///.*(?:\n[^\S\r\n]*///.*)*)\n[^\S\r\n]*#?let (\w[\w\d\-_]*)`.text) 139 | // The regex explained: 140 | // 141 | // First capture group: ([^\S\r\n]*///.*(?:\n[^\S\r\n]*///.*)*) 142 | // is for the docstring. It may start with any whitespace [^\S\r\n]* 143 | // and needs to have /// followed by anything. This is the first line of 144 | // the docstring and we treat it separately only in order to be able to 145 | // match the very first line in the file (which is otherwise tricky here). 146 | // We then match basically the same thing n times: \n[^\S\r\n]*///.*)* 147 | // 148 | // We then want a linebreak (should also have \r here?), arbitrary whitespace 149 | // and the word let or #let: \n[^\S\r\n]*#?let 150 | // 151 | // Second capture group: (\w[\w\d\-_]*) 152 | // Matches the function name (any Typst identifier) 153 | 154 | 155 | #let argument-type-matcher = regex(`[^\S\r\n]*/{3} - ([\w\d\-_]+) \(([\w\d\-_ ,]+)\): ?(.*)`.text) 156 | 157 | #let reference-matcher = regex(`@@([\w\d\-_\)\(]+)`.text) 158 | 159 | 160 | #let process-function-references(text, label-prefix: none) = { 161 | return text.replace(reference-matcher, info => { 162 | let target = info.captures.at(0).trim(")").trim("(") 163 | return "#link(label(\"" + label-prefix + target + "()\"))[`" + target + "()`]" 164 | }) 165 | } 166 | 167 | /// Parse the docstrings of Typst code. This function returns a dictionary with the keys 168 | /// - `functions`: A list of function documentations as dictionaries. 169 | /// - `label-prefix`: The prefix for internal labels and references. 170 | /// 171 | /// The function documentation dictionaries contain the keys 172 | /// - `name`: The function name. 173 | /// - `description`: The functions docstring description. 174 | /// - `args`: A dictionary of info objects for each function argument. 175 | /// 176 | /// These again are dictionaries with the keys 177 | /// - `description` (optional): The description for the argument. 178 | /// - `types` (optional): A list of accepted argument types. 179 | /// - `default` (optional): Default value for this argument. 180 | /// 181 | /// See @@show-module() for outputting the results of this function. 182 | /// 183 | /// - content (string): Typst code to parse for docs. 184 | /// - label-prefix (none, string): Prefix for internally created labels 185 | /// and references. Use this to avoid name conflicts with labels. 186 | #let parse-code(content, label-prefix: none) = { 187 | let matches = content.matches(docstring-matcher) 188 | let function-docs = () 189 | 190 | for match in matches { 191 | let docstring = match.captures.at(0) 192 | let fn-name = match.captures.at(1) 193 | 194 | let args = parse-argument-list(content, match.end) 195 | 196 | let fn-desc = "" 197 | let started-args = false 198 | let documented-args = () 199 | let return-types = none 200 | for line in docstring.split("\n") { 201 | let match = line.match(argument-type-matcher) 202 | if match == none { 203 | let trimmed-line = line.trim().trim("/") 204 | if not started-args { fn-desc += trimmed-line + "\n"} 205 | else { 206 | if trimmed-line.trim().starts-with("->") { 207 | return-types = trimmed-line.trim().slice(2).split(",").map(x => x.trim()) 208 | } else { 209 | documented-args.last().desc += "\n" + trimmed-line 210 | } 211 | } 212 | } else { 213 | started-args = true 214 | let param-name = match.captures.at(0) 215 | let param-types = match.captures.at(1).split(",").map(x => x.trim()) 216 | let param-desc = match.captures.at(2) 217 | documented-args.push((name: param-name, types: param-types, desc: param-desc)) 218 | } 219 | } 220 | fn-desc = process-function-references(fn-desc, label-prefix: label-prefix) 221 | for arg in documented-args { 222 | if arg.name in args { 223 | args.at(arg.name).description = process-function-references(arg.desc, label-prefix: label-prefix) 224 | args.at(arg.name).types = arg.types 225 | } 226 | } 227 | function-docs.push((name: fn-name, description: fn-desc, args: args, return-types: return-types)) 228 | } 229 | let result = (functions: function-docs, label-prefix: label-prefix) 230 | return result 231 | } 232 | 233 | /// Parse the docstrings of a typst module. This function returns a dictionary with the keys 234 | /// - `name`: The module name as a string. 235 | /// - `functions`: A list of function documentations as dictionaries. 236 | /// The label prefix will automatically be the name of the module. /// 237 | /// See @@parse-code() for more details. 238 | /// 239 | /// - filename (string): Filename for the `.typ` file to analyze for docstrings. 240 | /// - name (string, none): The name for the module. If not given, the module name will be derived form the filename. 241 | #let parse-module(filename, name: none) = { 242 | let mname = filename.replace(".typ", "") 243 | let result = parse-code(read(filename), label-prefix: mname) 244 | if name != none { 245 | result.insert("name", name) 246 | } else { 247 | result.insert("name", mname) 248 | } 249 | return result 250 | } 251 | 252 | 253 | 254 | /// Show given module in the style of the Typst online documentation. 255 | /// This displays all (documented) functions in the module sorted alphabetically. 256 | /// 257 | /// - module-doc (dictionary): Module documentation information as returned by @@parse-module. 258 | /// - first-heading-level (integer): Level for the module heading. Function names are created as second-level headings and the "Parameters" heading is two levels below the first heading level. 259 | /// - show-module-name (boolean): Whether to output the name of the module. 260 | /// - type-colors (dictionary): Colors to use for each type. 261 | /// Colors for missing types default to gray (`"#eff0f3"`). 262 | /// - allow-breaking (boolean): Whether to allow breaking of parameter description blocks 263 | /// - omit-empty-param-descriptions (boolean): Whether to omit description blocks for 264 | /// Parameters with empty description. 265 | /// -> content 266 | #let show-module( 267 | module-doc, 268 | first-heading-level: 2, 269 | show-module-name: true, 270 | type-colors: type-colors, 271 | allow-breaking: true, 272 | omit-empty-param-descriptions: true, 273 | ) = { 274 | let label-prefix = module-doc.label-prefix 275 | if "name" in module-doc and show-module-name { 276 | let module-name = module-doc.name 277 | heading(module-name, level: first-heading-level) 278 | } 279 | 280 | for (index, fn) in module-doc.functions.enumerate() { 281 | [ 282 | #heading(fn.name, level: first-heading-level + 1) 283 | #label(label-prefix + fn.name + "()") 284 | ] 285 | parbreak() 286 | eval("[" + fn.description + "]") 287 | 288 | block(breakable: allow-breaking, 289 | { 290 | heading("Parameters", level: first-heading-level + 2) 291 | 292 | pad(x:10pt, { 293 | set text(font: "Cascadia Mono", size: 0.85em, weight: 340) 294 | text(fn.name, fill: fn-color) 295 | "(" 296 | let inline-args = fn.args.len() < 2 297 | if not inline-args { "\n " } 298 | let items = () 299 | for (arg, info) in fn.args { 300 | let types 301 | if "types" in info { 302 | types = ": " + info.types.map(x => type-box(x)).join(" ") 303 | } 304 | items.push(arg + types) 305 | } 306 | items.join( if inline-args {", "} else { ",\n "}) 307 | if not inline-args { "\n" } + ")" 308 | if fn.return-types != none { 309 | " -> " 310 | fn.return-types.map(x => type-box(x)).join(" ") 311 | } 312 | }) 313 | }) 314 | 315 | let blocks = () 316 | for (name, info) in fn.args { 317 | let types = info.at("types", default: ()) 318 | let description = info.at("description", default: "") 319 | if description.trim() == "" and omit-empty-param-descriptions { continue } 320 | param-description-block( 321 | name, 322 | types, description, 323 | show-default: "default" in info, 324 | default: info.at("default", default: none), 325 | breakable: allow-breaking 326 | ) 327 | } 328 | if index < module-doc.functions.len() { v(1cm) } 329 | } 330 | } 331 | 332 | -------------------------------------------------------------------------------- /plotst/axis.typ: -------------------------------------------------------------------------------- 1 | // This sign can't stop me if I can't read 2 | #import "util/util.typ": * 3 | #import "@preview/oxifmt:0.2.0": strfmt 4 | 5 | //------------------ 6 | // THIS FILE CONTAINS EVERYTHING TO DRAW AND REPRESENT AXES 7 | //------------------ 8 | 9 | 10 | /// This is the constructor function for creating axes. Most plots/graphs will require axes to function. \ \ 11 | /// === Basics 12 | /// The most important parameters are `min`, `max`, `step` and `location`. These need most likely be changed for a functioning axis. If `min`, `max` and `step` are set, the `values` parameter will automatically be filled with the correct values. \ 13 | /// _Example:_ \ 14 | /// ```js let x_axis = axis(min: 0, max: 11, step: 2, location: "bottom")``` \ 15 | /// will cause `values` to look like this: \ 16 | /// `(0, 2, 4, 6, 8, 10)` \ \ 17 | /// If you want to specify your own values, for example when using text on an axis, you need to specify `values` by yourself. Custom specified values could look like this `("", "male", "female", "divers", "unknown")` (the first empty string is not necessary, but will make some graphs/plots look a lot better). \ \ 18 | /// You can obviously do a lot more than just this, so I recommend taking a look at the examples. \ \ 19 | /// === Examples 20 | /// An x-axis for different genders: 21 | /// ```typc 22 | /// let gender_axis_x = axis( 23 | /// values: ("", "m", "w", "d"), 24 | /// location: "bottom", 25 | /// helper_lines: true, 26 | /// invert_markings: false, 27 | /// title: "Gender" 28 | /// ) 29 | /// ``` \ 30 | /// A y-axis displaying ascending numbers: \ 31 | /// ```typc 32 | /// let y_axis_2 = axis(min: 0, max: 41, step: 10, 33 | /// location: "left", show_markings: true, helper_lines: true)``` 34 | /// 35 | /// *NOTE:* this might change to kebab-case 36 | /// 37 | /// - min (integer, float): From where `values` should started generating (inclusive) 38 | /// - max (integer, float): Where `values` should stopped being generated (exclusive) 39 | /// - step (integer, float): The steps that should be taken when generating `values` 40 | /// - values (array): The values of the markings (exclusive with `min`,#sym.space `max` and `step`) 41 | /// - location (string): The position of the axis. Only valid options are: `"top", "bottom", "left", "right"` 42 | /// - show_values (boolean): If the values should be displayed 43 | /// - show_arrows (boolean): If arrows at the end of axis should be displayed 44 | /// - show_markings (boolean): If the markings should be displayed 45 | /// - invert_markings (boolean): If the markins should point away from the data (outwards) 46 | /// - marking_offset_left (integer): Amount of hidden markings from the left or bottom 47 | /// - marking_offset_right (integer): Amount of hidden markings from the right or top 48 | /// - stroke (length, color, dictionary, stroke): The color of the baseline for the axis 49 | /// - marking_color (color): The color of the marking 50 | /// - value_color (color): The color of a value 51 | /// - helper_lines (boolean): If helper lines (to see better alignment of data) should be displayed 52 | /// - helper_line_style (string): The style of the helper lines, valid options are: `"solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dash-dotted", "densely-dash-dotted", "loosely-dash-dotted"` 53 | /// - helper_line_color (color): The color of the helper line 54 | /// - marking_length (length): The length of a marking in absolute size 55 | /// - marking_number_distance (length): The distance between the marker and the number 56 | /// - title (content): The display name of the axis 57 | /// - value_formatter (string, function): How values get displayed; uses https://github.com/typst/packages/tree/main/packages/preview/oxifmt/0.2.0 or a mapper function 58 | #let axis(min: 0, max: 0, step: 1, values: (), location: "bottom", show_values: true, show_arrows: true, show_markings: true, invert_markings: false, marking_offset_left: 1, marking_offset_right: 0, stroke: black, marking_color: black, value_color: black, helper_lines: false, helper_line_style: "dotted", helper_line_color: gray, marking_length: 5pt, marking_number_distance: 5pt, title: [], value_formatter: i => i) = { // TODO automate? macro-programming? 59 | let axis_data = ( 60 | min: min, 61 | max: max, 62 | step: step, 63 | location: location, 64 | show_values: show_values, 65 | show_arrows: show_arrows, 66 | show_markings: show_markings, 67 | invert_markings: invert_markings, 68 | marking_offset_left: marking_offset_left, 69 | marking_offset_right: marking_offset_right, 70 | stroke: stroke, 71 | marking_color: marking_color, 72 | value_color: value_color, 73 | helper_lines: helper_lines, 74 | helper_line_style: helper_line_style, 75 | helper_line_color: helper_line_color, 76 | marking_length: marking_length, 77 | marking_number_distance: marking_number_distance, 78 | title: title, 79 | values: values, 80 | value_formatter: value_formatter, 81 | ) 82 | 83 | if values.len() == 0 { 84 | axis_data.values = float_range(min, max, step: step) 85 | } 86 | 87 | return axis_data 88 | } 89 | 90 | #let format(axis, value) = { 91 | let fmt = axis.value_formatter 92 | if type(fmt) == "string" { 93 | return strfmt(fmt, value) 94 | } else if type(fmt) == "function" { 95 | return fmt(value) 96 | } 97 | } 98 | 99 | // returns true if and only if the axis is on the left or right, false if top or bottom, panics otherwise 100 | // axis: the axis 101 | #let is_vertical(axis) = { 102 | if axis.location == "left" or axis.location == "right" {return true} 103 | if axis.location == "top" or axis.location == "bottom" {return false} 104 | panic("axis location wrong") 105 | } 106 | 107 | 108 | // returns the expected need of space as a (width, height) array 109 | // axis: the axis 110 | // style: styling 111 | #let measure_axis(axis, style) = { 112 | let invert_markings = 1 113 | if axis.location == "right" { 114 | invert_markings = -1 115 | } 116 | if axis.location == "top" { 117 | invert_markings = -1 118 | } 119 | 120 | let dist = if axis.invert_markings {axis.marking_length + axis.marking_number_distance} else {axis.marking_number_distance} 121 | let inversion = if axis.invert_markings == -1 {dist * 2 + size.width} else {0pt} 122 | 123 | let title_extra = measure(axis.title).height 124 | 125 | let sizes = axis.values.map(it => { 126 | let size = measure([#format(axis, it)]) 127 | if is_vertical(axis) { 128 | return size.width 129 | } else { 130 | return size.height 131 | } 132 | }) 133 | let size = calc.max(..sizes) + inversion + 2 * dist + title_extra 134 | if is_vertical(axis) { 135 | return (size, 0pt) 136 | } else { 137 | return (0pt, size) 138 | } 139 | } 140 | 141 | //------------------------------ 142 | // AXIS DRAWING 143 | //------------------------------- 144 | 145 | // axis: the axis to draw 146 | // length: the length of the axis (mostly gotten from the plot code function; see util.typ, prepare_plot()) 147 | // pos: the position offset as an array(x, y) 148 | #let draw_axis(axis, length: 100%, pos: (0pt, 0pt)) = { 149 | 150 | let step_length = length / axis.values.len() 151 | let invert_markings = 1 152 | let user_invert_markings = if axis.invert_markings {-1} else {1} 153 | // Changes point of reference if top or right is chosen 154 | if axis.location == "right" { 155 | pos.at(0) = length - pos.at(0) 156 | invert_markings = -1 157 | } 158 | if axis.location == "top" { 159 | pos.at(1) = -length + pos.at(1) 160 | invert_markings = -1 161 | } 162 | let arrow_size = 0pt 163 | if axis.show_arrows { 164 | // sets the size of arrows 165 | arrow_size = 2pt 166 | } 167 | if is_vertical(axis) { 168 | // Places the axis line 169 | place(dx: pos.at(0), dy: pos.at(1), line(angle: -90deg, length: length - arrow_size * 2, stroke: axis.stroke)) 170 | 171 | // draw the arrow at the end of the axis 172 | if axis.show_arrows { 173 | place(dx: pos.at(0), dy: pos.at(1) - length, 174 | polygon(fill: axis.stroke, (-arrow_size, arrow_size * 2), (0pt, -1pt), (arrow_size, arrow_size * 2)) 175 | ) 176 | } 177 | // Places the title 178 | //place(dy: -50%, rotate(-90deg, axis.title)) // TODO 179 | context { 180 | let a = measure_axis(axis, style).at(0) 181 | if axis.location == "left" { 182 | place(dy: pos.at(1) - length / 2, dx: -length/2 - a, rotate(-90deg, origin: center + top, box(width: length, height:0pt, align(center+top, axis.title)))) 183 | } else { 184 | place(dy: pos.at(1) - length / 2, dx: length/2 +a, rotate(-90deg, origin: center + top, box(width: length, height:0pt, align(center+bottom, axis.title)))) 185 | } 186 | } 187 | 188 | // Draws step markings 189 | for step in range(axis.marking_offset_left, axis.values.len() - axis.marking_offset_right) { 190 | // Draw helper lines: 191 | if axis.helper_lines { 192 | //place(dx: pos.at(0), dy: pos.at(1) - step_length * step, line(angle: 0deg, length: length * invert_markings, stroke: (paint: axis.helper_line_color, dash: axis.helper_line_style))) 193 | } 194 | 195 | // Draw markings 196 | if axis.show_markings { 197 | place(dx: pos.at(0), dy: pos.at(1) - step_length * step, line(angle: 0deg, length: axis.marking_length * invert_markings * user_invert_markings, stroke: axis.marking_color)) 198 | } 199 | // Draw numbering 200 | if axis.show_values { 201 | let number = [#format(axis, axis.values.at(step))] 202 | context { 203 | let size = measure(number) 204 | let dist = if axis.invert_markings {axis.marking_length + axis.marking_number_distance} else {axis.marking_number_distance} 205 | let inversion = if invert_markings == -1 {dist * 2 + size.width} else {0pt} 206 | place(dx: pos.at(0) - dist - size.width + inversion, dy: pos.at(1) - step_length * step - 4pt, text(fill: axis.value_color, number)) 207 | } 208 | } 209 | } 210 | 211 | } else { 212 | // Places the axis line 213 | place(dx: pos.at(0), dy: pos.at(1), line(angle: 0deg, length: length - arrow_size * 2, stroke: axis.stroke)) 214 | 215 | // draw the arrow at the end of the axis 216 | if axis.show_arrows { 217 | place(dx: pos.at(0) + length - arrow_size * 2, dy: pos.at(1), 218 | polygon(fill: axis.stroke, (0pt, -arrow_size), (arrow_size * 2, 0pt), (0pt, arrow_size)) 219 | ) 220 | } 221 | 222 | // Places the title 223 | //place(dx: 50%, align(bottom + center, box(width:0pt, height: 0pt, axis.title))) // TODO willbreak 224 | if axis.location == "bottom" { 225 | place(dx: pos.at(0), dy: 3pt, align(top + center, box(width: length, height: 0pt, [\ #axis.title]))) 226 | } else { 227 | style(style => { 228 | let a = measure_axis(axis, style).at(1) 229 | layout(size => place(dy: -size.height - a, align(top + center, box(width: length, height: 0pt, [#axis.title])))) 230 | }) 231 | } 232 | 233 | // Draws step markings 234 | for step in range(axis.marking_offset_left, axis.values.len() - axis.marking_offset_right) { 235 | // Draw helper lines: 236 | if axis.helper_lines { 237 | //place(dx: pos.at(0) + step_length * step, dy: pos.at(1), line(angle: 90deg, length: length * -invert_markings, stroke: (paint: axis.helper_line_color, dash: axis.helper_line_style))) 238 | } 239 | 240 | // Draw markings 241 | if axis.show_markings { 242 | place(dx: pos.at(0) + step_length * step, dy: pos.at(1), line(angle: 90deg, length: axis.marking_length * -invert_markings * user_invert_markings, stroke: axis.marking_color)) 243 | } 244 | 245 | // Show values 246 | if axis.show_values { 247 | let number = axis.values.at(step) 248 | context { 249 | let size = measure([#number]) 250 | let dist = if axis.invert_markings {axis.marking_number_distance + axis.marking_length} else {axis.marking_number_distance} 251 | let inversion = if invert_markings == -1 {-dist * 2 - size.height} else {0pt} 252 | place(dx: pos.at(0) + step_length * step, dy: pos.at(1) + dist + inversion, box(width: 0pt, align(center, text(fill: axis.value_color, str(number))))) 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | // Draws the helper lines for an axis. Needs to be separated for rendering order reasons 260 | #let draw_helper_lines(axis, length: 100%, pos: (0pt, 0pt)) = { 261 | let step_length = length / axis.values.len() 262 | let invert_markings = 1 263 | // Changes point of reference if top or right is chosen 264 | if axis.location == "right" { 265 | pos.at(0) = length - pos.at(0) 266 | invert_markings = -1 267 | } 268 | if axis.location == "top" { 269 | pos.at(1) = -length + pos.at(1) 270 | invert_markings = -1 271 | } 272 | if is_vertical(axis) { 273 | // Draw helper lines: 274 | for step in range(axis.marking_offset_left, axis.values.len() - axis.marking_offset_right) { 275 | if axis.helper_lines { 276 | place(dx: pos.at(0), dy: pos.at(1) - step_length * step, line(angle: 0deg, length: length * invert_markings, stroke: (paint: axis.helper_line_color, dash: axis.helper_line_style))) 277 | } 278 | } 279 | } else { 280 | for step in range(axis.marking_offset_left, axis.values.len() - axis.marking_offset_right) { 281 | if axis.helper_lines { 282 | place(dx: pos.at(0) + step_length * step, dy: pos.at(1), line(angle: 90deg, length: length * -invert_markings, stroke: (paint: axis.helper_line_color, dash: axis.helper_line_style))) 283 | } 284 | } 285 | } 286 | } 287 | // ------------------------ 288 | -------------------------------------------------------------------------------- /plotst/plotting.typ: -------------------------------------------------------------------------------- 1 | #import "axis.typ": * 2 | #import "util/classify.typ": * 3 | #import "util/util.typ": * 4 | #import calc: * 5 | 6 | // hackyish solution to split axis and content 7 | #let render(plot, plot_code, render_axis, helper_line) = context { 8 | let widths = 0pt 9 | let heights = 0pt 10 | let offset_left = 0pt 11 | let offset_bottom = 0pt 12 | // Draw coordinate system 13 | for axis in plot.axes { 14 | let (w,h) = measure_axis(axis, style) 15 | if(axis.location == "left") { 16 | offset_left += w 17 | } 18 | widths += w 19 | if(axis.location == "bottom") { 20 | offset_bottom += h 21 | } 22 | heights += h 23 | } 24 | 25 | let x_axis = plot.axes.filter(it => not is_vertical(it)).first() 26 | let y_axis = plot.axes.filter(it => is_vertical(it)).first() 27 | 28 | let offset_y = 0pt 29 | let offset_x = 0pt 30 | if x_axis.location == "bottom" { 31 | offset_y = -offset_bottom 32 | } 33 | if y_axis.location == "left" { 34 | offset_x = offset_left 35 | } 36 | place(dx: offset_x, dy: 100% - offset_bottom, box(width: 100% - widths, height: 100% - heights, fill: none, { 37 | if helper_line { 38 | for axis in plot.axes { 39 | draw_helper_lines(axis) 40 | } 41 | } 42 | if render_axis { 43 | for axis in plot.axes { 44 | draw_axis(axis) 45 | } 46 | } else { 47 | plot_code() 48 | } 49 | })) 50 | } 51 | 52 | // Prepares everything for a plot and executes the function that draws a plot. Supplies it with width and height 53 | // size: the size of the plot either as array(width, height) or length 54 | // caption: the caption for the plot 55 | // capt_dist: distance from plot to caption 56 | //------- 57 | // width: the width of the plot 58 | // height: the height of the plot 59 | // the plot code: a function that needs to look accept parameters (width, height) 60 | // plot: if set this function will attempt to render the axes and prepare everything. If not, the setup is up to you 61 | // if you want to make the axes visible (only if plot is set) 62 | //------- 63 | #let prepare_plot(size, caption, plot_code, plot: (), render_axis: true) = { 64 | let (width, height) = if type(size) == "array" {size} else {(size, size)} 65 | figure(caption: caption, supplement: "Graph", kind: "plot", { 66 | // Graph box 67 | set align(left + bottom) 68 | box(width: width, height: height, fill: none, if plot == () { plot_code() } else { 69 | if render_axis { render(plot, plot_code, true, true) } 70 | render(plot, plot_code, false, false) 71 | }) 72 | }) 73 | } 74 | 75 | 76 | 77 | /// The constructor function for a plot. This combines the `data` with the `axes` you need to display a graph/plot. The exact structure of `axes` and `data` varies from the visual representation you choose. An exact specification of how these have to look will be found there. 78 | /// === Examples 79 | /// This is how your plot initialisation will look most of the time: 80 | /// ```typc 81 | /// let x_axis = axis(…) 82 | /// let y_axis = axis(…) 83 | /// let data = (…) 84 | /// let pl = plot(axes: (x_axis, y_axis), data: data) ``` \ 85 | /// How your plot initialisation would look for a _pie chart_: 86 | /// ```typc 87 | /// let data = (…) 88 | /// let pl = plot(data: data)``` \ 89 | /// This is a lot simpler ans a _pie chart_ doesn't require any axes. \ \ 90 | /// - axes (axis): A list of axes needed for drawing the plot (most likely a x- and y-axis) 91 | /// - data (array): The data that should be mapped onto the plot. The format depends on the plot type 92 | #let plot(axes: (), data: ()) = { 93 | let plot_data = ( 94 | axes: axes, 95 | data: data 96 | ) 97 | return plot_data 98 | } 99 | 100 | /// This function is used to overlay multiple plots. This can be used to render multiple graph lines in one plot and much more. The axes that get rendered, are the axes of the first plot inserted. Make sure all plots use the same axes as otherwise this will cause issues. 101 | /// - plots (array): An array of all the `plot` objects you want to render. 102 | /// - plot_types (array): An array of the different types of plots these should be rendered as. This array needs to have the same length, as the `plots` array. The array accepts the following strings: `scatter, graph, histogram, pie, bar`. The type of plot is applied per index. 103 | /// - size (length, array): The size as array of `(width, height)` or as a single value for both `width` and `height` 104 | #let overlay(plots, size) = { 105 | figure(caption: plots.at(0).caption, supplement: "Graph", kind: "plot", { 106 | set align(left + bottom) 107 | box(..convert_size(size), { 108 | // loop over every plots 109 | for (idx, plot) in plots.enumerate() { 110 | if idx == 0 { 111 | place(dx: 0pt, dy: 0pt, box(width: 100%, height: 100%, plot.body.child.body)) 112 | } else { 113 | place(dx: 0pt, dy: 0pt, box(width: 100%, height: 100%, plot.body.child.body.children.at(1))) 114 | } 115 | } 116 | }) 117 | }) 118 | } 119 | 120 | /// This function will display a scatter plot based on the provided `plot` object. 121 | /// === How to create a simple scatter plot 122 | /// First, we need to define the data we want to map to the scatter plot. In this case I will use some random sample data. \ 123 | /// ```typc let data = ((0, 0), (1, 2), (2, 4), (3, 6), (4, 8), (5, 3), (6, 6),(7, 9),(11, 12))``` \ \ 124 | /// Next, we need to define both the x and the y-axis. The x-axis location can either be `"bottom"` or `"top"`. The y-axis location can either be `"left"` or `"right"`. You can customise the look of the axes with `axis` specific parameters (here: `helper_lines: true`)\ 125 | /// ```typc let x_axis = axis(min: 0, max: 11, step: 1, location: "bottom") 126 | /// let y_axis = axis(min: 0, max: 13, step: 2, location: "left", helper_lines: true)``` 127 | /// Now we need to create a `plot` object based on the axes and the data. \ 128 | /// ```typc let pl = plot(axes: (x_axis, y_axis), data: data) ``` 129 | /// 130 | /// Last, we need to just call this function. In this case the width of the plot will be `100%` and the height will be `33%`. \ 131 | /// ```typc scatter_plot(pl, (100%, 33%))``` \ \ 132 | /// - plot (plot): The format of the plot variables are as follows: \ 133 | /// - `axes:` Two axes are required. The first one as the x-axis, the second as the y-axis. \ _Example:_ `(x_axis, y_axis)` 134 | /// - `data:` An array of `x` and `y` pairs. \ _Example:_ `((0, 0), (1, 2), (2, 4), …)` 135 | /// - size (length, array): The size as array of `(width, height)` or as a single value for both `width` and `height` 136 | /// - caption (content): The name of the figure 137 | /// - stroke (none, auto, length, color, dictionary, stroke): The stroke color of the dots (deprecated) 138 | /// - fill (color): The fill color of the dots (deprecated) 139 | /// - render_axes (boolean): If the axes should be visible or not 140 | /// - markings (string, content): how the data points should be shown: "square", "circle", "cross", otherwise manually specify any shape (gets overwritten by stroke/fill) 141 | #let scatter_plot(plot, size, caption: [Scatter Plot], stroke: none, fill: none, render_axes: true, markings: "square") = { 142 | let x_axis = plot.axes.at(0) 143 | let y_axis = plot.axes.at(1) 144 | // The code rendering the plot 145 | let plot_code() = { 146 | let step_size_x = calc_step_size(100%, x_axis) 147 | let step_size_y = calc_step_size(100%, y_axis) 148 | // Places the data points 149 | for (x,y) in plot.data { 150 | if type(x) == "string" { 151 | x = x_axis.values.position(c => c == x) 152 | } 153 | if type(y) == "string" { 154 | y = y_axis.values.position(c => c == y) 155 | } 156 | if stroke != none or fill != none { // DELETEME deprecation, only keep else 157 | draw_marking(((x - x_axis.min) * step_size_x, -(y - y_axis.min) * step_size_y), square(width: 2pt, height: 2pt, fill: fill, stroke: stroke)) 158 | } else { 159 | draw_marking(((x - x_axis.min) * step_size_x, -(y - y_axis.min) * step_size_y), markings) 160 | } 161 | } 162 | } 163 | 164 | // Sets outline for a plot and defines width and height and executes the plot code 165 | prepare_plot(size, caption, plot_code, plot: plot, render_axis: render_axes) 166 | } 167 | 168 | /// This function will display a graph plot based on the provided `plot` object. It functions like the _scatter plot_ but connects the dots with lines. 169 | /// === How to create a simple graph plot 170 | /// First, we need to define the data we want to map to the graph plot. In this case I will use some random sample data. \ 171 | /// ```typc let data = ((0, 0), (1, 2), (2, 4), (3, 6), (4, 8), (5, 3), (6, 6),(7, 9),(11, 12))``` \ \ 172 | /// Next, we need to define both the x and the y-axis. The x-axis location can either be `"bottom"` or `"top"`. The y-axis location can either be `"left"` or `"right"`. You can customise the look of the axes with `axis` specific parameters (here: `helper_lines: true`) 173 | /// ```typc let x_axis = axis(min: 0, max: 11, step: 1, location: "bottom") 174 | /// let y_axis = axis(min: 0, max: 13, step: 2, location: "left", helper_lines: true)``` 175 | /// Now we need to create a `plot` object based on the axes and the data. \ 176 | /// ```typc let pl = plot(axes: (x_axis, y_axis), data: data) ```\ \ 177 | /// Last, we need to just call this function. In this case the width of the plot will be `100%` and the height will be `33%`. \ 178 | /// ```typc graph_plot(pl, (100%, 33%))``` \ \ 179 | /// - plot (plot): The format of the plot variables are as follows: \ 180 | /// - `axes:` Two axes are required. The first one as the x-axis, the second as the y-axis. \ _Example:_ `(x_axis, y_axis)` 181 | /// - `data:` An array of `x` and `y` pairs. \ _Example:_ `((0, 0), (1, 2), (2, 4), …)` 182 | /// - size (length, array): The size as array of `(width, height)` or as a single value for both `width` and `height` 183 | /// - caption (content): The name of the figure 184 | /// - rounding (ratio): The rounding of the graph, 0% means sharp edges, 100% will make it as smooth as possible (Bézier) 185 | /// - stroke (none, auto, length, color, dictionary, stroke): How to stoke the graph. \ See: #link("https://typst.app/docs/reference/visualize/line/#parameters-stroke") 186 | /// - fill (color): The fill color for the graph. Can be used to display the area beneath the graph. 187 | /// - render_axes (boolean): If the axes should be visible or not 188 | /// - markings (none, string, content): how the data points should be shown: "square", "circle", "cross", otherwise manually specify any shape 189 | #let graph_plot(plot, size, caption: "Graph Plot", rounding: 0%, stroke: black, fill: none, render_axes: true, markings: "square") = { 190 | let x_axis = plot.axes.at(0) 191 | let y_axis = plot.axes.at(1) 192 | let plot_code() = { 193 | let step_size_x = calc_step_size(100%, x_axis) 194 | let step_size_y = calc_step_size(100%, y_axis) 195 | // Places the data points 196 | let data = plot.data.map(((x,y)) => { 197 | if type(x) == "string" { 198 | x = x_axis.values.position(c => c == x) 199 | } 200 | if type(y) == "string" { 201 | y = y_axis.values.position(c => c == y) 202 | } 203 | ((x - x_axis.min) * step_size_x, -(y - y_axis.min) * step_size_y) 204 | }) 205 | let delta = () 206 | let rounding = rounding * -1 207 | for i in range(data.len()) { 208 | let curr = data.at(i) 209 | let next 210 | let prev 211 | if i != data.len() - 1 { next = data.at(i + 1) } 212 | if i != 0 { prev = data.at(i - 1) } 213 | if i == 0 { 214 | delta.push((rounding * (next.at(0) - curr.at(0)), rounding * (next.at(1) - curr.at(1)))) 215 | } else if i == data.len() - 1 { 216 | delta.push((rounding * (curr.at(0) - prev.at(0)), rounding * (curr.at(1) - prev.at(1)))) 217 | } else { 218 | delta.push((rounding * .5 * (next.at(0) - prev.at(0)), rounding * .5 * (next.at(1) - prev.at(1)))) 219 | } 220 | } 221 | 222 | place(dx: 0pt, dy: 0pt, path(fill: fill, stroke: stroke, ..data.zip(delta))) 223 | for p in data { 224 | draw_marking(p, markings) 225 | } 226 | } 227 | 228 | prepare_plot(size, caption, plot_code, plot: plot, render_axis: render_axes) 229 | } 230 | 231 | /// This function will display a histogram based on the provided `plot` object. \ \ 232 | /// === How to create a simple histogram 233 | /// First, we need to define the data and the classes we want to map to the graph plot. In this case I will use some random sample data. \ The tricky part about this is, that this data gets represented in `classes`. These are necessary to combine the data the right way, so the bars height can be displayed correctly. \ Here, I will use the same class size every time but once. \ \ 234 | /// Let's create the data now: 235 | /// ```typc let data = ( 236 | /// 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 18000, 237 | /// 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 28000, 238 | /// 35000, 46000, 75000, 95000 239 | /// ) ``` \ 240 | /// Now, we will define the classes. To do this we can use the `class_generator(start, end, amount)` and the `class(lower_lim, upper_lim) `function (_see `classify.typ`_) 241 | /// ```typc let classes = class_generator(10000, 50000, 4) 242 | /// classes.push(class(50000, 100000)) 243 | /// classes = classify(data, classes)``` 244 | /// This will result in creating the following classes: `(10000 - 20000, 20000 - 30000, 30000 - 40000, 40000 - 50000, 50000 - 100000)`. \ \ 245 | /// Next, we need to define both the x and the y-axis. The x-axis location can either be `"bottom"` or `"top"`. The y-axis location can either be `"left"` or `"right"`. You can customise the look of the axes with `axis` specific parameters (here: `show_markings: true` and `helper_lines: true`) 246 | /// ```typc let x_axis = axis(min: 0, max: 100000, step: 20000, location: "bottom", show_markings: false) 247 | /// let y_axis = axis(min: 0, max: 26, step: 3, location: "left", helper_lines: true)``` \ 248 | /// Now we need to create a `plot` object based on the axes and the data. \ 249 | /// ```typc let pl = plot(axes: (x_axis, y_axis), data: data) ``` \ \ 250 | /// Last, we just need to call this function. Here we render the histogram with a black outline around the bars, and a gray filling of the bars. \ 251 | /// ```typc histogram(pl, (100%, 20%), stroke: black, fill: gray) ``` \ \ 252 | /// 253 | /// - plot (plot): The format of the plot variables are as follows: \ 254 | /// - `axes:` Two axes are required. The first one as the x-axis, the second as the y-axis. \ _Example:_ `(x_axis, y_axis)` 255 | /// - `data:` An array of `x` and `y` pairs. \ _Example:_ `((0, 0), (1, 2), (2, 4), …)` 256 | /// - size (length, array): The size as array of `(width, height)` or as a single value for both `width` and `height` 257 | /// - caption (content): The name of the figure 258 | /// - stroke (none, auto, length, color, dictionary, stroke, array): The stroke color of a bar or an `array` of colors, where every entry stands for the stroke color of one bar 259 | /// - fill (color, array): The fill color of a bar or an `array` of colors, where every entry stands for the fill color of one bar 260 | /// - render_axes (boolean): If the axes should be visible or not 261 | #let histogram(plot, size, caption: [Histogram], stroke: black, fill: gray, render_axes: true) = { 262 | // Get the relevant axes: 263 | let x_axis = plot.axes.at(0) 264 | let y_axis = plot.axes.at(1) 265 | let plot_code() = { 266 | let step_size_x = calc_step_size(100%, x_axis) 267 | let step_size_y = calc_step_size(100%, y_axis) 268 | 269 | let array_stroke = type(stroke) == "array" 270 | let array_fill = type(fill) == "array" 271 | // Get count of values 272 | let val_count = 0 273 | for data in plot.data { 274 | val_count += data.data.len() 275 | } 276 | 277 | // Find most common class size 278 | // count class occurrences 279 | let bin_count = () 280 | for data in plot.data { 281 | let temp = data.upper_lim - data.lower_lim 282 | let found = false 283 | for (idx, entry) in bin_count.enumerate() { 284 | if temp == entry.at(1) { 285 | bin_count.at(idx).at(0) += 1 286 | found = true 287 | break 288 | } 289 | } 290 | if not found { 291 | bin_count.push((1, temp)) 292 | } 293 | } 294 | // find most common one 295 | let common_class = bin_count.at(0) 296 | for value in bin_count { 297 | common_class = if value.at(0) > common_class.at(0) {value} else {common_class} 298 | } 299 | // get the size of the most common class 300 | common_class = common_class.at(1) 301 | 302 | // place the bars 303 | for (idx, data) in plot.data.enumerate() { 304 | let width = (data.upper_lim - data.lower_lim) * step_size_x 305 | 306 | let rel_H = (data.data.len() / val_count) 307 | let bin_width = (data.upper_lim - data.lower_lim) 308 | let height = (data.data.len() * (common_class / bin_width)) * step_size_y 309 | let dx = data.lower_lim * step_size_x 310 | place(dx: dx, dy: -height, rect(width: width, height: height, fill: if array_fill {fill.at(idx)} else {fill}, stroke: if array_stroke {stroke.at(idx)} else {stroke})) 311 | } 312 | } 313 | 314 | prepare_plot(size, caption, plot_code, plot: plot, render_axis: render_axes) 315 | } 316 | 317 | 318 | /// This function will display a pie chart based on the provided `plot` object. \ \ 319 | /// === How to create a simple pie chart 320 | /// This is the easiest diagram to create. First we need to specify the data. I will use random data here. \ 321 | /// ```typc let data = ((10, "Male"), (20, "Female"), (15, "Divers"), (2, "Other")) ``` \ \ 322 | /// Because no axes are required, we can skip this step and jump straight to creating the `plot`. 323 | /// ```typc let p = plot(data: data) ``` \ \ 324 | /// Last, we just need to call this function. I will call it with all styles available. 325 | /// ```typc pie_chart(p, (100%, 20%), display_style: "legend-inside-chart") 326 | /// pie_chart(p, (100%, 20%), display_style: "hor-chart-legend") 327 | /// pie_chart(p, (100%, 20%), display_style: "hor-legend-chart") 328 | /// pie_chart(p, (100%, 20%), display_style: "vert-chart-legend") 329 | /// pie_chart(p, (100%, 20%), display_style: "vert-legend-chart")``` \ 330 | /// - plot (plot): The format of the plot variables are as follows: \ 331 | /// - `axes:` No axes are required. 332 | /// - `data:` An array of single values or an array of `(amount, value)` tuples. \ _Example:_ `((10, "Male"), (5, "Female"), (2, "Divers"), …)` or `("Male", "Male", "Male", "Female", "Female", "Divers", "Divers", …)` 333 | /// - size (length, array): The size as array of `(width, height)` or as a single value for both `width` and `height` 334 | /// - caption (content): The name of the figure 335 | /// - display_style (string): Changes the style of the pie chart. Available are: `"vert-chart-legend", "hor-chart-legend", "vert-legend-chart", "hor-legend-chart", "legend-inside-chart"`. 336 | /// - colors (array): The colors used in the pie chart. If not enough colors were specified, the colors get repeated. 337 | /// - offset (length): The distance from the center to the text in the pie chart (only relevant when using `"legend-inside-chart"`) 338 | #let pie_chart(plot, size, caption: [Pie chart], display_style: "hor-chart-legend", colors: (red, blue, green, yellow, purple, orange), offset: 50%) = { 339 | // get a point on a radius 1 circle 340 | //--- 341 | // x: travelled distance around circle 342 | let point(x) = ( 343 | calc.cos(x), calc.sin(x) 344 | ) 345 | 346 | // calculate the points needed on a bezier curve to gain an approximation of a circle 347 | //--- 348 | // radius: the radius of the circle 349 | // start_angle: the angle at which the circle starts (counting from right going clockwise) 350 | // length: the angle of the segment 351 | let segment(radius, start_angle, length) = { 352 | let details = 4 //number of details, 3 is minimum 353 | let points = ((0pt, 0pt),) 354 | let distance = (4 / 3) * calc.tan(length / details / 4) 355 | 356 | for i in range(0, details + 1) { 357 | 358 | let angle = length / details * i + start_angle 359 | let pnt = point(angle) 360 | let delta = point(angle - 90deg) 361 | 362 | points.push(( 363 | (pnt.at(0) * radius, pnt.at(1) * radius), 364 | (delta.at(0) * radius * distance, delta.at(1) * radius * distance) 365 | )) 366 | 367 | } 368 | points.at(-1).push((0pt, 0pt)) // fix end corner 369 | 370 | let temp = points.at(1).at(1) 371 | points.at(1).at(1) = (0pt, 0pt) 372 | points.at(1).push((-temp.at(0), -temp.at(1))) 373 | points.push((0pt, 0pt)) 374 | return points 375 | } 376 | 377 | let data = plot.data 378 | 379 | if not type(data.at(0)) == "array" { 380 | let new_data = ((0, data.at(0)),) 381 | for value in data { 382 | let found = false 383 | for (idx, existing) in new_data.enumerate() { 384 | if existing.at(1) == value { 385 | new_data.at(idx).at(0) += 1 386 | found = true 387 | break 388 | } 389 | } 390 | if not found { 391 | new_data.push((1, value)) 392 | } 393 | } 394 | data = new_data 395 | } 396 | 397 | // The code rendering the plot 398 | let plot_code() = { 399 | set align(center + top) 400 | layout(size => { 401 | box(width: size.width, height: size.height) 402 | let total = data.map(a => a.at(0)).sum() 403 | let angle = 0deg 404 | 405 | let radius = min( 406 | size.width, 407 | if display_style.split("-").at(0) == "vert" {size.height - 30pt} else {size.height}) / 2 408 | let pie = [] 409 | let legend = [] 410 | let dx = radius 411 | if display_style == "legend-inside-chart" { 412 | dx = 50% 413 | } 414 | for (i, data) in data.enumerate() { 415 | let fraction = data.at(0) / total * 360deg 416 | let points = segment(radius, angle, fraction) 417 | pie += place(dy: -radius, dx: dx, path(fill: colors.at(calc.rem(i, colors.len())), ..points)) 418 | 419 | if display_style == "legend-inside-chart" { 420 | let pnt = point(angle + fraction / 2) 421 | pie += place(dx: pnt.at(0) * radius * offset + dx, dy: pnt.at(1) * radius * offset - radius, box(width: 0pt, height: 0pt, align(center + horizon, box(fill: purple, str(data.at(1)))))) 422 | } else { 423 | legend += text(fill: colors.at(calc.rem(i, colors.len())), sym.hexa.filled) + sym.space.thin + str(data.at(1)) 424 | if i != plot.data.len() - 1 { legend += if "hor" in display_style { "\n" } else { sym.space.quad } } 425 | } 426 | angle += fraction 427 | } 428 | context { 429 | let legend-size = measure(legend) 430 | if display_style == "vert-chart-legend" { 431 | place(dx: 50% - radius, dy: -30pt, pie) 432 | place(dx: 50% - legend-size.width / 2, dy: -20pt, legend) 433 | } else if display_style == "vert-legend-chart" { 434 | place(dx: 50% - legend-size.width / 2, dy: -(radius * 2) - 20pt, legend) 435 | place(dx: 50% - radius, dy: 0pt, pie) 436 | } else if display_style == "hor-chart-legend" { 437 | place(dx: 50% - radius - legend-size.width / 2, dy: 0pt, pie) 438 | place(dx: 50% + radius - legend-size.width / 2 + 10pt, dy: -radius, box(height: 0pt, align(horizon, legend))) 439 | } else if display_style == "hor-legend-chart" { 440 | place(dx: 50% - radius + legend-size.width / 2, dy: 0pt, pie) 441 | place(dx: 50% - radius - legend-size.width / 2 - 10pt, dy: -radius, box(height: 0pt, align(horizon, legend))) 442 | } else if display_style == "legend-inside-chart" { 443 | pie 444 | } else { 445 | panic(display_style + " is not a valid display_style") 446 | } 447 | 448 | } 449 | 450 | }) 451 | } 452 | 453 | // Sets outline for a plot and defines width and height and executes the plot code 454 | prepare_plot(size, caption, plot_code) 455 | } 456 | 457 | /// This function will display a bar chart based on the provided `plot` object. \ \ 458 | /// === How to create a simple bar chart 459 | /// First we need to specify the data, we want to display. I will use some random data here.\ 460 | /// ```typc let data = ((20, 2), (30, 3), (16, 4), (40, 6), (5, 7))``` \ \ 461 | /// Next we need to create the axes. Keep in mind that, if you want to make the bars go from left to right, not bottom to top, you need to basically invert the x and y-axis creation. You can also customise the axes (here: `show_markings: true` and `helper_lines: true`). 462 | /// ```typc let x_axis = axis(min: 0, max: 9, step: 1, location: "bottom") 463 | /// let y_axis = axis(min: 0, max: 41, step: 10, location: "left", show_markings: true, helper_lines: true)``` 464 | /// When `rotated: true`, in other words the bars grow from left to right, the axis creation looks like this: 465 | /// ```typc let x_axis = axis(min: 0, max: 41, step: 10, location: "bottom", show_markings: true, helper_lines: true) 466 | /// let y_axis = axis(min: 0, max: 9, step: 1, location: "left")``` \ 467 | /// Now we need to create the `plot` object. \ 468 | /// ```typc let pl = plot(axes: (x_axis, y_axis), data: data)``` \ \ 469 | /// Last, we just call this function to display the chart. We specify fill colors for every single bar to make it easier to differentiate and we make the bars 30% smaller to create small gaps between bars close to each other. \ 470 | /// ```typc bar_chart(pl, (100%, 120pt), fill: (purple, blue, red, green, yellow), bar_width: 70%)``` \ \ 471 | /// - plot (plot): The format of the plot variables are as follows: \ 472 | /// - `axes:` Two axes are required. The first one as the x-axis, the second as the y-axis. \ _Example:_ `(x_axis, y_axis)` 473 | /// - `data:` An array of single values or an array of `(amount, value)` tuples. \ _Example:_ `((10, "Male"), (5, "Female"), (2, "Divers"), …)` or `("Male", "Male", "Male", "Female", "Female", "Divers", "Divers", …)` 474 | /// - size (length, array): The size as array of `(width, height)` or as a single value for both `width` and `height` 475 | /// - caption (content): The name of the figure 476 | /// - stroke (none, auto, length, color, dictionary, stroke, array): The stroke color of a bar or an `array` of colors, where every entry stands for the stroke color of one bar 477 | /// - fill (color, array): The fill color of a bar or an `array` of colors, where every entry stands for the fill color of one bar 478 | /// - centered_bars (boolean): If the bars should be on the number its corresponding to 479 | /// - bar_width (ratio): how thick the bars should be in percent. (default: 100%) 480 | /// - rotated (boolean): If the bars should grow on the `x_axis` - this means the data gets mapped to the `y-axis`. Don't forget to create the axes accordingly. 481 | /// - render_axes (boolean): If the axes should be visible or not 482 | #let bar_chart(plot, size, caption: "Barchart", stroke: black, fill: gray, centered_bars: true, bar_width: 100%, rotated: false, render_axes: true) = { 483 | // Get the relevant axes: 484 | let x_axis = plot.axes.at(0) 485 | let y_axis = plot.axes.at(1) 486 | 487 | let plot_code() = { 488 | // get step sizes 489 | let step_size_x = calc_step_size(100%, x_axis) 490 | let step_size_y = calc_step_size(100%, y_axis) 491 | // get correct data 492 | let data = transform_data_count(plot.data) 493 | let array_stroke = type(stroke) == "array" 494 | let array_fill = type(fill) == "array" 495 | // draw the bars 496 | if not rotated { 497 | for (idx, data_set) in data.enumerate() { 498 | let height = data_set.at(0) * step_size_y 499 | let x_data = data_set.at(1) 500 | if type(data_set.at(1)) == "string" { 501 | x_data = x_axis.values.position(c => c == x_data) 502 | } 503 | let x_pos = x_data * step_size_x - if centered_bars {step_size_x * bar_width / 2} else {0pt} 504 | place(dx: x_pos, dy: -height, 505 | rect(width: step_size_x * bar_width, height: height, 506 | fill: if array_fill {fill.at(idx)} else {fill}, 507 | stroke: if array_stroke {stroke.at(idx)} else {stroke})) 508 | } 509 | } else { 510 | for (idx, data_set) in data.enumerate() { 511 | let width = data_set.at(0) * step_size_x 512 | let y_data = data_set.at(1) 513 | if type(data_set.at(1)) == "string" { 514 | y_data = y_axis.values.position(c => c == y_data) 515 | } 516 | let y_pos = y_data * step_size_y + if centered_bars {step_size_y * bar_width / 2} else {0pt} 517 | place(dx:0pt, dy: -y_pos, rect(width: width, height: step_size_y * bar_width, 518 | fill: if array_fill {fill.at(idx)} else {fill}, 519 | stroke: if array_stroke {stroke.at(idx)} else {stroke})) 520 | } 521 | } 522 | } 523 | prepare_plot(size, caption, plot_code, plot: plot, render_axis: render_axes) 524 | } 525 | 526 | /// This function will display a graph plot based on the provided `plot` object. It functions like the _scatter plot_ but connects the dots with lines in a circular fashion. 527 | /// === How to create a simple radar plot 528 | /// First, we need to define the data we want to map to the graph plot. In this case I will use some random sample data. \ 529 | /// ```typc let data = ((0,6),(1,7),(2,5),(3,4),(4,4),(5,7),(6,6),(7,1),)``` \ \ 530 | /// Next, we need to define both the x and the y-axis. You can customise the look of the axes with `axis` specific parameters (here: `helper_lines: true`) 531 | /// ```typc let y_axis = axis(min:0, max: 8, location: "left", helper_lines: true) 532 | /// let x_axis = axis(min:0, max: 8, location: "bottom")``` 533 | /// Now we need to create a `plot` object based on the axes and the data. \ 534 | /// ```typc let pl = plot(data: data, axes: (x_axis, y_axis))```\ \ 535 | /// Last, we need to just call this function. In this case the width of the plot will be `100%` and the height will be `33%`. \ 536 | /// ```typc radar_chart(pl, (100%, 33%))``` \ \ 537 | /// - plot (plot): The format of the plot variables are as follows: \ 538 | /// - `axes:` Two axes are required. The first one as the x-axis, the second as the y-axis. \ _Example:_ `(x_axis, y_axis)` 539 | /// - `data:` An array of `x` and `y` pairs. \ _Example:_ `((0, 0), (1, 2), (2, 4), …)` 540 | /// - size (length, array): The size as array of `(width, height)` or as a single value for both `width` and `height` 541 | /// - caption (content): The name of the figure 542 | /// - stroke (none, auto, length, color, dictionary, stroke): The stroke color of the graph 543 | /// - fill (color): The fill color for the graph. Can be used to display the area beneath the graph. 544 | /// - render_axes (boolean): If the axes should be visible or not 545 | /// - markings (none, string, content): how the data points should be shown: "square", "circle", "cross", otherwise manually specify any shape 546 | /// - scaling (ratio): how much the actual plot should be smaller to account for axis namings 547 | #let radar_chart(plot, size, caption: "Radar Chart", stroke: black, fill: none, render_axes: true, markings: "square", scaling: 95%) = { 548 | let x_axis = plot.axes.at(0) 549 | let y_axis = plot.axes.at(1) 550 | let plot_code() = { 551 | let data = plot.data.map(((x,y)) => { 552 | if type(x) == "string" { 553 | x = x_axis.values.position(c => c == x) 554 | } 555 | if type(y) == "string" { 556 | y = y_axis.values.position(c => c == y) 557 | } 558 | (x,y) 559 | }) 560 | place(dy: -50%, dx: 50%, box(height: scaling, width: scaling, { 561 | layout(size => { 562 | let radius = min(size.width, size.height)/2 563 | place( { 564 | let last = plot.data.at(-1) 565 | 566 | let x_size = x_axis.values.len() 567 | let y_size = y_axis.values.len() 568 | let step_size = radius / y_size 569 | 570 | let translate(x,y) = { 571 | (calc.sin(360deg/x_size * x) * y * step_size, -calc.cos(360deg/x_size * x) * y * step_size) 572 | } 573 | 574 | place(box(height: 50%, width: 0pt, draw_axis(y_axis)), dy: -50%) 575 | for x in range(1,x_size) { 576 | place(line(angle: 360deg/x_size * x -90deg, length: radius)) 577 | } 578 | if x_axis.show_values { 579 | for i in range(0, x_size) { 580 | let (x,y) = translate(i, y_size) 581 | place(dx: x / float(scaling), dy: y / float(scaling), box(width: 0pt, height: 0pt, align(center + horizon, text(str(x_axis.values.at(i)), fill: x_axis.value_color)))) 582 | } 583 | } 584 | for y in range(1, y_size) { 585 | let points = () 586 | for x in range(x_size) { 587 | points += (translate(x,y),) 588 | if(y_axis.show_markings) { 589 | place(line(start: points.at(-1), angle: 360deg/x_size*x, length: y_axis.marking_length)) 590 | place(line(start: points.at(-1), angle: 360deg/x_size*x, length: -y_axis.marking_length)) 591 | } 592 | } 593 | if(y_axis.helper_lines) { 594 | place(path(..points, closed: true, stroke: (paint: y_axis.helper_line_color, dash: y_axis.helper_line_style))) 595 | } 596 | } 597 | 598 | let points = () 599 | for p in data { 600 | let (x,y) = translate(p.at(0)/x_axis.step, p.at(1)/y_axis.step) 601 | points += ((x,y),) 602 | draw_marking((x,y), markings) 603 | } 604 | place(path(..points, closed: true, stroke: stroke, fill: fill)) 605 | }) 606 | }) 607 | })) 608 | } 609 | 610 | prepare_plot(size, caption, plot_code, plot: plot, render_axis: false) 611 | } 612 | 613 | 614 | /// This function will display a boxplot based on the provided `plot` object. 615 | #let box_plot(plot, size, caption: "Box plot", stroke: black, fill: none, whisker_stroke: black, box_width: 100%, pre_calculated: true, render_axes: true) = { 616 | // Get the relevant axes: 617 | let x_axis = plot.axes.at(0) 618 | let y_axis = plot.axes.at(1) 619 | 620 | let plot_code() = { 621 | // get step sizes 622 | let step_size_x = calc_step_size(100%, x_axis) 623 | let step_size_y = calc_step_size(100%, y_axis) 624 | // only data containing (minimum, first_quartile, median, third_quartile, maximum) 625 | // get correct data 626 | let calc_data = plot.data.map(dataset => transform_data_full(dataset).sorted()) 627 | let data = calc_data.map(dataset => ( 628 | dataset.at(0), 629 | if calc.rem(dataset.len() * 0.25, 1) != 0 { 630 | dataset.at(int(dataset.len() * 0.25))} 631 | else { 632 | (dataset.at(int(dataset.len() * 0.25) - 1) + dataset.at(int(dataset.len() * 0.25))) / 2}, 633 | if calc.rem(dataset.len() * 0.25, 1) != 0 { 634 | dataset.at(int(dataset.len() * 0.5))} 635 | else { 636 | (dataset.at(int(dataset.len() * 0.5) - 1) + dataset.at(int(dataset.len() * 0.5))) / 2}, 637 | if calc.rem(dataset.len() * 0.25, 1) != 0 { 638 | dataset.at(int(dataset.len() * 0.75))} 639 | else { 640 | (dataset.at(int(dataset.len() * 0.75) - 1) + dataset.at(int(dataset.len() * 0.75))) / 2}, 641 | dataset.at(dataset.len() - 1) 642 | )) 643 | data = if pre_calculated {plot.data} else {data} 644 | 645 | // let data = transform_data_count(plot.data) 646 | let array_stroke = type(stroke) == "array" 647 | let array_fill = type(fill) == "array" 648 | // draw the boxes 649 | data = if type(data.at(0)) == "array" { 650 | data 651 | } else { 652 | (data,) 653 | } 654 | for (idx, data_set) in data.enumerate() { 655 | let q(i) = (data_set.at(i) - y_axis.min) * step_size_y 656 | let x_data = data_set.at(5, default: idx + 1) 657 | if type(x_data) == "string" { 658 | x_data = x_axis.values.position(c => c == x_data) 659 | } 660 | let box_width = step_size_x * box_width 661 | let whisk_width = box_width * 50% 662 | let x_pos = x_data * step_size_x - box_width / 2 663 | place(dx: x_pos, dy: -q(3), 664 | rect(width: box_width, height: q(3) - q(1), 665 | fill: if array_fill {fill.at(idx)} else {fill}, 666 | stroke: if array_stroke {stroke.at(idx)} else {stroke})) 667 | place(dx: x_pos, dy: -q(2), line(length: box_width)) 668 | place(dx: x_pos + (box_width - whisk_width) * .5, dy: -q(0), line(length: whisk_width)) 669 | place(dx: x_pos + box_width * .5, dy: -q(0), line(end: (0pt, q(0)-q(1)), stroke: whisker_stroke)) 670 | place(dx: x_pos + (box_width - whisk_width) * .5, dy: -q(4), line(length: whisk_width)) 671 | place(dx: x_pos + box_width * .5, dy: -q(3), line(end: (0pt, q(3)-q(4)), stroke: whisker_stroke)) 672 | } 673 | } 674 | prepare_plot(size, caption, plot_code, plot: plot, render_axis: render_axes) 675 | } 676 | --------------------------------------------------------------------------------