diff --git a/docs/Project.toml b/docs/Project.toml index ce3ad5a3733..54e2bd62113 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -26,4 +26,5 @@ NodeJS = "2bd173c7-0d6d-553b-b6af-13a54713934c" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" RPRMakie = "22d9f318-5e34-4b44-b769-6e3734a732a6" RadeonProRender = "27029320-176d-4a42-b57d-56729d2ad457" +Typst_jll = "eb4b1da6-20f6-5c66-9826-fdb8ad410d0e" WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" diff --git a/docs/makedocs.jl b/docs/makedocs.jl index 9a600603633..5ceb92f3170 100644 --- a/docs/makedocs.jl +++ b/docs/makedocs.jl @@ -187,6 +187,7 @@ pages = [ "explanations/transparency.md", ], "How-Tos" => [ + "how-to/match-figure-size-font-sizes-and-dpi.md", "how-to/draw-boxes-around-subfigures.md", "how-to/save-figure-with-transparency.md", ], @@ -197,25 +198,29 @@ pages = [ ] ] -empty!(MakieDocsHelpers.FIGURES) - -# filter pages here when working on docs interactively -# pages = nested_filter(pages, r"reference/blocks/(axis|axis3|overview)") - -Documenter.makedocs(; - sitename="Makie", - format=DocumenterVitepress.MarkdownVitepress(; - repo = "github.com/MakieOrg/Makie.jl", - devurl = "dev", - devbranch = "master", - deploy_url = "https://docs.makie.org", # for local testing not setting this has broken links with Makie.jl in them - description = "Create impressive data visualizations with Makie, the plotting ecosystem for the Julia language. Build aesthetic plots with beautiful customizable themes, control every last detail of publication quality vector graphics, assemble complex layouts and quickly prototype interactive applications to explore your data live.", - deploy_decision, - ), - pages, - expandfirst = unnest(nested_filter(pages, r"reference/(plots|blocks)/(?!overview)")), - warnonly = get(ENV, "CI", "false") != "true", - pagesonly = true, +function make_docs(; pages) + empty!(MakieDocsHelpers.FIGURES) + + Documenter.makedocs(; + sitename="Makie", + format=DocumenterVitepress.MarkdownVitepress(; + repo = "github.com/MakieOrg/Makie.jl", + devurl = "dev", + devbranch = "master", + deploy_url = "https://docs.makie.org", # for local testing not setting this has broken links with Makie.jl in them + description = "Create impressive data visualizations with Makie, the plotting ecosystem for the Julia language. Build aesthetic plots with beautiful customizable themes, control every last detail of publication quality vector graphics, assemble complex layouts and quickly prototype interactive applications to explore your data live.", + deploy_decision, + ), + pages, + expandfirst = unnest(nested_filter(pages, r"reference/(plots|blocks)/(?!overview)")), + warnonly = get(ENV, "CI", "false") != "true", + pagesonly = true, + ) +end + +make_docs(; + # filter pages here when working on docs interactively + pages # = nested_filter(pages, r"explanations/figure|match-figure"), ) ## diff --git a/docs/src/explanations/figure.md b/docs/src/explanations/figure.md index 757393dabb6..0feb5521d54 100644 --- a/docs/src/explanations/figure.md +++ b/docs/src/explanations/figure.md @@ -152,52 +152,102 @@ contents(f[1, 1]) == [ax] content(f[1, 1]) == ax ``` -## Figure size and units +## Figure size and resolution -In Makie, figure size and attributes like line widths, font sizes, scatter marker extents, or layout column and row gaps are usually given as plain numbers, without an explicit unit attached. -What does it mean to have a `Figure` with `size = (600, 450)`, a line with `linewidth = 10` or a column gap of `30`? +In Makie, the **size** of a `Figure` is unitless. That is because `Figure`s can be rendered as images or vector graphics or displayed in interactive windows. The actual physical size of those outputs depends on multiple factors such as screen sizes which are often outside of Makie's control, so we don't want to promise correct output size under all circumstances for a hypothetical `Figure(size = (4cm, 5cm))`. -The first underlying idea is that, no matter what your final output format is, these numbers are _relative_. -You can expect a `linewidth = 10` to cover 1/60th of the width `600` of the `Figure` and a column gap of `30` to span 1/20th of the Figure. -This holds, no matter if you later export that `Figure` as an image made out of pixels, or as a vector graphic that doesn't have pixels at all. +The `size` of a `Figure` first and foremost tells you how much space there is for `Axis` objects and other content. For example, `fontsize` or `Axis(width = ..., height = ...)` are also unitless, but they can be understood relative to the figure size. If there is not enough space in your `Figure`, you can either increase its `size` or decrease the size of the content (for example with smaller fontsizes). However, we don't _only_ care about the `size` of the `Figure` relative to the sizes of its contents. It also has a meaning when we think about how big or small our `Figure` will look when rendered on all the different possible output devices. -The second idea is that, given some `Figure`, we want to be able to export an image at arbitrary resolution, or a vector graphic at any size from it, as long as the relative sizes of all elements stay intact. -So we need to _translate_ our abstract sizes to real sizes when we render. -In Makie, this is done with two scaling factors: `px_per_unit` for images and `pt_per_unit` for vector graphics. +Now, although Makie uses unitless numbers for figure size, it is set up by default such that these numbers can actually be thought of as CSS pixels. We have chosen this convention to simplify using Makie in web contexts which includes browser-based tools like Pluto, Jupyter notebooks or editors like VSCode. All of these use CSS to control the appearance of objects. -A line with `linewidth = 10` will be 10 pixels wide if rendered to an image file with `px_per_unit = 1`. It will be 5 pixels wide if `px_per_unit = 0.5` and 20 pixels if `px_per_unit = 2`. A `Figure` with `size = (600, 450)` will have 600 x 450 pixels when exported with `px_per_unit = 1`, 300 x 225 with `px_per_unit = 0.5` and 1200 x 900 with `px_per_unit = 2`. +At default settings, a `Figure` of size `(600, 450)` will be displayed at a size of 600 x 450 CSS pixels in web contexts (if Makie renders via the `text/html` or `image/svg+xml` MIME types). This is true irrespective of its **resolution**, i.e., how many pixels the output bitmap has. The image will be annotated with `width = "600px" height = "450px"` so that browsers will know the intended display size. -It works exactly the same for vector graphics, just with a different target unit. A `pt` or point is a typographic unit that is defined as 1/72 of an inch, which comes out to about 0.353 mm. A line with `linewidth = 10` will be 10 points wide if rendered to an svg file with `pt_per_unit = 1`, it will be 5 points wide for `pt_per_unit = 0.5` and 20 points wide if `pt_per_unit = 2`. A `Figure` with `size = (600, 450)` will be 600 x 450 points in size when exported with `pt_per_unit = 1`, 300 x 225 with `pt_per_unit = 0.5` and 1200 x 900 with `pt_per_unit = 2`. +The CSS pixel is a physical unit (1 px == 1/96 inch) but of course browsers display content on many different screens and at many different zoom levels, so you would usually not expect an element of 96px width to be exactly 1 inch wide at any given time. But even if we don't know what physical size our plots will have on our screens, we want them to fit in well next to other content and text, so we want to match the sizes conventionally used on today's systems. For example, a common fontsize is `12 pt`, which is equivalent to `16 px` (1 px == 3/4 pt). -### Defaults of `px_per_unit` and `pt_per_unit` +This also applies to pdf outputs. When preparing plots for publications, we usually want to match font sizes of their plots to the base document, for example 12pt. But today we don't usually print pdfs on paper at their intended physical dimensions. Often, they are read on mobile devices where they are zoomed in and out, so any given text will rarely be at 12pt physically. -What are the default values of `px_per_unit` and `pt_per_unit` in each Makie backend, and why are they set that way? +While vector graphics are always rendered sharply at a given zoom level, for bitmaps, the actual number of pixels decides at what zoom level or viewing distance they look sharp or blurry. This "sharpness" factor is often specified in `dpi` or dots per inch. Again, the "inch" here should not be expected to always match an actual physical inch (like in the printing days) because of the way we zoom in and out on digital screens. But if we conventionally use CSS pixels to describe sizes, we can also use `dpi` and we'll know what sharpness to expect on typical devices and typical zoom levels. -Let us start with `pt_per_unit` because this value is only relevant for one backend, which is CairoMakie. -The default value in CairoMakie is `pt_per_unit = 0.75`. So if you `save("output.svg", figure)` a `Figure` with `size = (600, 450)`, this comes out as a vector graphic that is 450 x 337.5 pt large. +To sum up, we have two factors that affect the rendered output of a Makie `Figure`. Its **size**, which determines the space available for content and the display size when interpreted in units like CSS pixels, and the **resolution** or sharpness in terms of pixel density or `dpi`. For vector graphics we only care about the size factor (unless we're embedding rasterized bitmaps in them). -Why 0.75 and not simply 1? This has to do with web standards and device-independent pixels. Websites mix vector graphics and images, so they need some way to relate the sizes of both types to each other. In principle, a pixel in an image doesn't have a real-world width. But you don't want the images on your site to shrink relative to the other content when device pixels are small, or grow when device pixels are large. So web browsers don't directly map image pixels to device pixels. Instead, they use a concept called device-independent pixels. If you place an image with 600 x 450 pixels in a website, this image is interpreted by default to be 600 x 450 device-independent pixels wide. One device-independent pixel is defined to be 0.75 pt wide, that's where the factor 0.75 comes in. So an image with 600 x 450 device-independent pixels is the same apparent size as a vector graphic with size 450 x 337.5 pt. On high-resolution screens, browsers then simply render one device-independent pixel with multiple device pixels (for example 2x2 on an Apple Retina display) so that content stays at readable sizes and doesn't look tiny. +### The `px_per_unit` factor -For Makie, we decided that we want our abstract units to match device-independent pixels when used in web contexts, because that's very convenient and easy to predict for the end user. If you have a Jupyter or Pluto notebook, it's nice if a `Figure` comes out at the same apparent size, no matter if you're currently in CairoMakie's svg mode, or in the bitmap mode of any backend. Therefore, we annotate images with the original `Figure` size in device-independent pixels, so they are of the same apparent size, no matter what the `px_per_unit` value and therefore the effective pixel size is. And we give svg files the default scaling factor of 0.75 so that svgs always match images in apparent size. +If we display a `Figure(size = (600, 450))` in a web context, by Makie's convention the image will be annotated with `width = "600px" height = "450px"`. But how many pixels does the actual bitmap have, i.e., how sharp is the image? -Now let us look at the default values for `px_per_unit`. In CairoMakie, the default is `px_per_unit = 2`. This means, a `Figure` with `size = (600, 450)` will be rendered as a 1200 x 900 pixel image. The reason it isn't `px_per_unit = 1` is that CairoMakie plots are often embedded in notebooks or websites, or looked at in image viewers or IDEs like VSCode. On websites, you don't know in advance what the pixel density of a reader's display is going to be. And in image viewers and IDEs, people like to zoom in to look at details. To cover these use cases by default, we decided `px_per_unit = 2` is a good compromise between sharp resolution and appropriate file size. Again, the _apparent_ size of output images in notebooks and websites (wherever the `MIME"text/html"` type is used) depends only on the `size`, because the output images are embedded with ` Increasing `size` gives you more space for your content and a larger bitmap. When scaled to the same size in an output context (a pdf document for example), a figure with larger `size` will appear to have smaller content. + +```@raw html + +``` + +> Increasing `px_per_unit` leaves the space for your content the same but gives a larger bitmap due to higher resolution. When scaled to the same size in an output context (a pdf document for example), a figure with larger `px_per_unit` will appear to have the same content, but sharper. + +```@raw html + +``` + +There is also a `pt_per_unit` factor with which you can scale the output for vector graphics up or down. But if you keep with the convention that Makie's unitless numbers are actually CSS pixels, you can leave the default `pt_per_unit` at 0.75 and get size-matched bitmaps and vector graphics automatically. diff --git a/docs/src/explanations/scenes.md b/docs/src/explanations/scenes.md index d637d957efe..9a829196324 100644 --- a/docs/src/explanations/scenes.md +++ b/docs/src/explanations/scenes.md @@ -17,7 +17,7 @@ A Scene's subscenes (also called children) can be accessed through `scene.childr Any `Scene` with an axis also has a `camera` associated with it; this can be accessed through `camera(scene)`, and its controls through `cameracontrols(scene)`. More documentation about these is in the [Cameras](@ref) section. -`Scene`s have a configurable size. You can set the size in device-independent pixels by doing `Scene(size = (500, 500))`. (More about sizes, resolutions and units in [Figure size and units](@ref)) +`Scene`s have a configurable size. You can set the size in device-independent pixels by doing `Scene(size = (500, 500))`. (More about sizes, resolutions and units in [Figure size and resolution](@ref) or [How to match Figure size, font sizes and dpi](@ref)) Any keyword argument given to the `Scene` will be propagated to its plots; therefore, you can set the palette or the colormap in the Scene itself. diff --git a/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md b/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md new file mode 100644 index 00000000000..7f9132c5848 --- /dev/null +++ b/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md @@ -0,0 +1,68 @@ +# How to match Figure size, font sizes and dpi + +We want to create three plots for inclusion in a document. These are the requirements: + +- Figure 1: png @ 4x3 inches and 100 dpi +- Figure 2: png @ 9x7 cm and 300 dpi +- Figure 3: svg @ 4x3 inches + +The fontsize of all three should match the document's 12pt setting. + +We assume the convention that Makie's unitless figure size is actually equivalent to CSS pixels. +For a deeper explanation why, check the section [Figure size and resolution](@ref). + +We're using Typst here but the technique applies similarly for all authoring tools that allow you to set the dimensions of included images. + +```@example +using CairoMakie +CairoMakie.activate!() # hide +using Typst_jll + +# these are relative to 1 CSS px +inch = 96 +pt = 4/3 +cm = inch / 2.54 + +f1 = Figure(size = (4inch, 3inch), fontsize = 12pt) +f2 = Figure(size = (9cm, 7cm), fontsize = 12pt) +f3 = Figure(size = (4inch, 3inch), fontsize = 12pt) + +titles = [ + "Figure 1: png @ 4x3 inches and 100 dpi", + "Figure 2: png @ 9x7 cm and 300 dpi", + "Figure 3: svg @ 4x3 inches", +] + +data = cumsum(randn(100)) + +for (f, title) in zip([f1, f2, f3], titles) + ax = Axis(f[1, 1]; title, xlabel = "time (s)", ylabel = "value (€)") + lines!(ax, data) +end + +save("figure1.png", f1, px_per_unit = 100/inch) +save("figure2.png", f2, px_per_unit = 300/inch) +save("figure3.svg", f3) + +typst_code = """ + #set page(fill: rgb("#f5f2eb")) + #set text(font: "TeX Gyre Heros Makie", size: 12pt, fill: luma(50%)) + + This is some text at 12pt which the figures below should match. + + #image("figure1.png", width: 4in, height: 3in) + #image("figure2.png", width: 9cm, height: 7cm) + #image("figure3.svg") // vector graphics have physical dimensions +""" + +open(io -> println(io, typst_code), "document.typ", "w") + +cp(Makie.assetpath("fonts", "TeXGyreHerosMakie-Regular.otf"), "./texgyre.otf") + +run(`$(Typst_jll.typst()) compile --font-path . document.typ output.svg`) + +nothing # hide +``` + +![](output.svg) +