Nomograms.jl GitHub

API#

Contents#

Index#

Nomograms.Nomograms

source
Nomograms

Build, solve and render nomograms (alignment charts).

A nomogram is a chart that computes by alignment: lay a straightedge across the scales at the values you know, and read the answer where it crosses the remaining scale. The scales are drawn so that any straight line crossing them picks out a solution of an equation. The geometry rests entirely on d'Ocagne's collinearity criterion,

| x1  y1  1 |
| x2  y2  1 | = 0
| x3  y3  1 |

This package therefore represents every scale as a parametric curve value -> (x, y), and does all its arithmetic by finding where a line crosses a curve. The same solver handles forward and inverse problems, and layouts other than the built-in parallel-axis construction need no extra code.

Getting started

using Nomograms

nom = additive_nomogram(
    Variable(:R1, ReciprocalScale(); domain = (1, 10), label = "R1 (ohm)"),
    Variable(:R2, ReciprocalScale(); domain = (1, 10), label = "R2 (ohm)"),
    Variable(:R, ReciprocalScale(); label = "R (ohm)");
    title = "Resistors in parallel",
)

evaluate(nom; R1 = 4, R2 = 6)          # -> (R1 = 4.0, R2 = 6.0, R = 2.4)
save_svg("parallel.svg", nom; isopleths = [isopleth(nom; R1 = 4, R2 = 6)])

See additive_nomogram, multiplicative_nomogram, evaluate, isopleth, svg and nomogramplot.

Nomograms.DEFAULT_ISOPLETH_COLORS

source
DEFAULT_ISOPLETH_COLORS

Blue, orange and aqua, in that order. All the isopleths on a chart are visible at once, so every pair of colours has to stay distinguishable under colour-vision deficiency, and no fourth hue does once every pair is taken into account. The colours were checked with a validator: the worst pair is ΔE 9.2 under deuteranopia and 24.0 in normal vision, against a target of at least 8.

d3's category10 palette is not used because its orange and green are only ΔE 0.7 apart under protanopia, which makes them look the same.

Nomograms.DEFAULT_ISOPLETH_DASHES

source
DEFAULT_ISOPLETH_DASHES

Dash patterns, cycled one step behind the colours, so a fourth isopleth is dashed blue instead of a second plain blue. Lines are never told apart by colour alone, so a chart can carry more lines than there are safe colours.

Nomograms.AbstractNomogram

source
AbstractNomogram

A chart made of Scales on a canvas. Drawing one (ticks, labels, bounding box and both backends) needs only the scales, the canvas and the title, so that code is written against this type. Subtypes differ in how they are solved: a Nomogram is one alignment line through every scale, while a CompoundNomogram is several stages read in turn.

Nomograms.ComposedScale

source
ComposedScale(outer, inner)

f(x) = outer(inner(x)). Used internally by multiplicative_nomogram to wrap a user transform in a logarithm, and available for building transforms such as ComposedScale(LogScale(), PowerScale(2)).

Nomograms.CompoundNomogram

source
CompoundNomogram

Several Nomogram stages sharing variables, for relations that need more than three variables.

A single alignment line relates at most three scales, so a four-variable relation is split into two, g(a, b) = t and h(t, c) = d, which share the intermediate t. The reader lays a line across the first stage to find where it crosses the shared scale, then pivots about that point and reads the second stage. This is why the shared scale is often called the pivot or turning line.

Build one with compound, which positions the stages so their shared scales coincide. Drawing works exactly as for a single nomogram; solving propagates values from stage to stage (see evaluate).

Nomograms.FunctionScale

source
FunctionScale(f, finv = nothing)

An arbitrary monotone f supplied by the user. Without finv, inversion is done by bracketed root finding over the variable's domain, so such a variable must declare its domain explicitly.

Nomograms.Isopleth

source
Isopleth

A straight line laid across a Nomogram through one point per scale: the visual form of a single solution of the relation. Build one with isopleth.

Fields

  • values::Dict{Symbol,Float64}: the value read off each scale.

  • points::Vector{Point}: where the line meets each scale, in scale order.

  • label::String

Nomograms.IsoplethDrawing

source
IsoplethDrawing

Laid-out geometry for one alignment line: the segment to stroke, the points where it meets each scale, and where its label sits.

Nomograms.LinearScale

source
LinearScale(a = 1, b = 0)

f(x) = a*x + b. The default transform, used for plain additive relations.

Nomograms.LogScale

source
LogScale(base = 10)

f(x) = log_base(x), defined for x > 0. Turns a product relation into an additive one.

Nomograms.Net

source
Net(u::Variable, v::Variable, surface)

A two-parameter family of points, surface(u, v) -> (x, y), drawn as a mesh of curves. It takes the place of one of the three scales an alignment line crosses.

Three collinear points relate three variables. A net carries two variables in a single point, so a chart with a net and two ordinary scales relates four. This is a different construction from compound, which chains stages through a shared pivot. A net can handle relations that a chain cannot: a pivot works only when the first two variables combine into one intermediate, while a net handles a true two-parameter family.

Deriving a net

With scale a on x = 0 at height f(a) and scale b on x = 1 at height g(b), a line between them meets x = x₃ at (1-x₃)·f(a) + x₃·g(b). So the line passes through a net point (x₃, y₃) exactly when

α·f(a) + β·g(b) = γ        with   x₃ = β/(α+β),  y₃ = γ/(α+β)

So any relation that can be written this way, with the coefficients α, β, γ depending on the two net variables, has a net. The mesh is truly two-dimensional as long as (α : β : γ) sweeps out a region. If it sweeps out only a curve, the net collapses to a line, and the relation needed compound instead.

See net_nomogram.

Nomograms.NetNomogram

source
NetNomogram

A Net and two Scales: four variables related by one alignment line.

Build one with net_nomogram. Drawing works as for any other chart: the net's strands are scales of kind = :grid, labelled with their values, and each family's name is drawn past the end of its row of labels. How evaluate solves the chart depends on which variable is unknown; see its docstring.

Nomograms.Nomogram

source
Nomogram

A set of Scales on a shared canvas, plus the relation they encode. Every scale is crossed by one alignment line.

Build one with additive_nomogram or multiplicative_nomogram, or directly from scales for layouts those constructions do not cover. For a relation needing more than three variables, see compound.

Nomograms.NomogramDrawing

source
NomogramDrawing

A complete, backend-independent description of a laid-out nomogram. It holds everything a renderer needs, but no drawing instructions. svg and the Makie extension both draw from this structure, and tests can check tick positions without rendering anything.

Nomograms.NomogramStyle

source
NomogramStyle(; kwargs...)

Sizes and thresholds for turning a Nomogram into geometry. All lengths are in chart units, the same units as a nomogram's width and height (70 by 100 by default), so one style works unchanged at any output size and in either backend.

The defaults follow the proportions of a standard d3 axis: hairline rules, short ticks and small type. At the default pxperunit = 4 a chart unit is 4 pixels, which gives roughly 1px strokes, 6px major ticks, 3px of label padding and 10px labels.

Keywords

  • majorticklength, minorticklength: tick mark lengths.

  • graded_ticks: whether ticks between those lengths are graded by importance, as on a ruler: wholes longest, then halves, quarters and so on on a fraction axis; 10s, 5s, then the rest on an integer one; decades, whole digits, then tenths on a logarithmic one. Labels sit the same distance from the axis whatever their tick's length. false gives every labelled tick the major length and every other tick the minor length.

  • ticklabelgap: gap between a tick's outer end and its label.

  • ticklabelsize, titlesize: text heights.

  • titlegap: gap between the end of an axis and its title.

  • min_major_spacing, min_minor_spacing: a tick is dropped if it would land closer than this to one already placed. This keeps a non-uniform axis readable.

  • label_minors: whether minor ticks get labels too.

  • endpoint_ticks: whether to add the ends of each domain as labelled ticks when they fit. An end that needs more than three significant figures is not labelled, since it is a derived limit such as 237.4 and not a value anyone chose.

  • ticklabelrotation: :horizontal leaves tick labels upright; :tangent turns each to run along its scale, which reads better on a steep or curved axis.

  • resolve_collisions: whether to drop tick labels that would overlap a label from another scale, and nudge isopleth labels clear of both.

  • avoid_isopleths: whether to also drop a tick label that an alignment line crosses. Such a label is hard to read, and the tick mark still shows where the value falls.

  • collisionpad: extra clearance required between two labels.

  • axislinewidth, gridlinewidth, tickwidth, isoplethwidth: stroke widths. The grid width applies to the strands of a Net. Its strands are read like any other scale, so by default they match the axes; make it thinner, or pass gridcolor to svg or nomogramplot, for a lighter net.

  • isopleth_overhang: how far an alignment line extends past the outermost scale.

  • markersize: radius of the dot where an isopleth meets a scale.

Nomograms.Point

source
Point

A position in chart coordinates, (x, y) with y increasing upwards.

Nomograms.PowerScale

source
PowerScale(p)

f(x) = x^p.

Nomograms.ReciprocalScale

source
ReciprocalScale()

f(x) = 1/x, defined away from zero. The transform behind parallel-resistance and thin-lens style charts.

Nomograms.Scale

source
Scale

A Variable placed on the page: a parametric curve value -> (x, y) over a resolved domain. A nomogram works entirely through the collinearity of one point on each scale, so every layout, whether parallel, curved or built by hand, comes down to a set of these.

Nomograms.Scale — Tuple{Variable, Any, Any, Symbol}

source
Scale(variable, domain, curve, side, kind = :scale)

kind is :scale for a scale a reader takes a value from, or :grid for one strand of a Net. Strands are read like scales and drawn like them, but carry no ticks, and a renderer's gridcolor can draw them lighter.

Nomograms.ScaleDrawing

source
ScaleDrawing

Laid-out geometry for one scale: the axis polyline, its ticks, and where its title goes.

Nomograms.ScaleTransform

source
ScaleTransform

A strictly monotone map from a variable's own units onto the functional axis of a nomogram, together with its inverse.

A three-variable alignment chart exists whenever the relation can be written in the separable form f1(u) + f2(v) = f3(w); a ScaleTransform is one of those fs. Because the inverse comes with the forward map, the construction can compute the third variable's domain exactly instead of searching for it.

Implement transform for a new subtype, and either invtransform or hasinverse(::T) = false (in which case inversion falls back to bracketed root finding over the variable's domain).

Nomograms.TabulatedScale

source
TabulatedScale

A ScaleTransform known only numerically: values and derivatives at a set of nodes, joined by cubic Hermite interpolation.

auto_nomogram builds these when it derives a transform by integration, because most of those integrals have no useful closed form. With exact derivatives at the nodes, the interpolation error is fourth order in the node spacing, far smaller than anything a chart can show. Outside the tabulated range it extrapolates linearly, so a solver probing an ulp past the edge of a domain gets an answer instead of an exception.

Nomograms.TextDrawing

source
TextDrawing

A title drawn on its own rather than off the end of a scale: the name of one of a net's families, whose strands carry only their values. In NomogramDrawing's texts.

Nomograms.TickMark

source
TickMark

One tick on a scale: where it sits, which way it points away from the axis, how long it is, and what (if anything) it is labelled with. level is 1 for major ticks and 2 for minor.

labelpos, align and rotation fully determine where the text goes, so every backend places it in the same way, and the layout can work out the box it occupies.

Nomograms.Variable

source
Variable(name, transform = LinearScale(); kwargs...)

One variable of a nomogram, before it has been placed on the page.

Arguments

  • name::Symbol: the keyword used to refer to this variable in evaluate and isopleth.

  • transform::ScaleTransform: the f that this variable contributes to the separable relation f1(u) + f2(v) = f3(w).

Keywords

  • domain: (lo, hi) in the variable's own units. May be nothing for the result variable of a construction, whose range is then derived from the other two. With Unitful loaded the ends may be quantities, (1u"ft", 10u"ft"), which also sets unit.

  • unit: the unit the axis is drawn in, such as u"ft" (needs Unitful). Values passed to evaluate and isopleth may then be quantities in any unit of the same dimension, results come back as quantities, and a plain number is read as being in this unit. Defaults to the unit of the domain's ends, if they have one.

  • label: axis title; defaults to string(name). The unit is drawn after it, length (ft), unless unitlabel = false.

  • ticks, minorticks: explicit tick values, bypassing automatic selection.

  • tickformat: value -> String. Defaults to fractionformat for tickstyle = :fraction and to plain numbers otherwise.

  • tickstyle: how automatic tick values are picked.

    • :linear or :log for round decimals spaced evenly or by decade;

    • :integer for whole numbers only, for a count such as gear teeth or turns;

    • :fraction for fractions whose denominator divides maxden (halves, quarters, eighths and sixteenths by default), labelled as fractions, such as 1 3/8 instead of 1.375;

    • :auto to take it from the transform. A construction may override :auto: for example, an N-chart's ratio axis gets logarithmic ticks when it spans a decade or more.

    Whichever is chosen, ticks are thinned by distance on the page, roundest first.

  • maxden = 16: for tickstyle = :fraction, the finest fraction offered; every denominator dividing it is used. 16 gives halves to sixteenths, 12 gives halves, thirds, quarters, sixths and twelfths.

A domain written with Rational ends, such as (1//10, 1//1), makes a variable left on tickstyle = :auto a :fraction one, with maxden the least common multiple of the ends' denominators (10 here), or 16 if they are all whole numbers. An explicit tickstyle or maxden takes precedence, and a floating-point end turns this off.

  • side: :left, :right, or :auto (the construction chooses).

  • titleend: which end of the scale the title goes past: :start or :stop for the low or high end of the domain, or :auto for whichever end is higher on the page. Set this when the automatic choice puts the title somewhere crowded, as happens for the strands of a Net that converge at one end.

Nomograms.additive_nomogram — Tuple{Variable, Variable, Variable}

source
additive_nomogram(v1, v2, v3; width = 70, height = 100, title = "", max_modulus_ratio = 4)

A three-parallel-axis nomogram for the separable relation

f1(v1) + f2(v2) = f3(v3)

where each fi is the ScaleTransform carried by the corresponding Variable. v3 is the result variable and is drawn as the middle axis; it may leave domain = nothing, in which case its range is derived from the other two.

The construction

Place v1 on x = 0 at y1 = a1 * (f1 - p0) and v2 on x = W at y2 = a2 * (f2 - q0), where [p0, p1] and [q0, q1] are the ranges of f1 and f2 over their domains and a1 = H/(p1-p0), a2 = H/(q1-q0) fit each to the canvas height H.

A straight line through those two points crosses x = L*W at

y = (1-L) * y1 + L * y2

Choosing L = a1 / (a1 + a2) makes (1-L)*a1 == L*a2 == a1*a2/(a1+a2) =: a3, so that

y = a3 * (f1 + f2) - a3 * (p0 + q0)

which is exactly a linear scale for f3 = f1 + f2 with gradient a3. Reading a value off the middle axis therefore solves the relation. Because a3 * (1/a1 + 1/a2) == 1, the middle axis spans the same [0, H] as the outer two: its height and position are determined by the outer scales and are not free parameters.

Keeping the middle axis clear

The middle axis sits at L = a1 / (a1 + a2) of the way across, so it lies close to whichever outer axis has the smaller gradient, which is the one whose f spans more. Over [1, 1000] against [11, 14] on logarithmic scales the gradients differ about 28-fold, and the middle axis lands 3% of the way across, nearly on top of its neighbour. So the steeper gradient is capped at max_modulus_ratio times the other, by shortening that axis and centring it vertically. An outer axis shifted by d shifts the line's crossing of the middle one by d times that axis's share, L or 1 - L, so the middle axis is shifted to match and every reading is unchanged. The middle axis then lies at least 1/(1 + max_modulus_ratio) of the width from either outer axis: a fifth, by default. The shortened axis only loses resolution it could spare, since it was the one stretched furthest. Pass max_modulus_ratio = Inf for full-height outer axes whatever their ranges.

Example

julia> nom = additive_nomogram(
           Variable(:a; domain = (0, 10)),
           Variable(:b; domain = (0, 20)),
           Variable(:total),
       );

julia> nom[:total].domain
(0.0, 30.0)

julia> round(evaluate(nom; a = 3, b = 4).total; digits = 10)
7.0

Nomograms.align_for — Tuple{Tuple{Float64, Float64}}

source
align_for(direction) -> (halign, valign)

Which corner of a text box sits on its anchor point, given the direction the text is pushed away from whatever it labels.

Both components matter. Using only the horizontal one, which is enough for vertical axes, centres a label vertically on its anchor. A label pushed straight down from a tick on a curved scale would then sit half on top of the tick.

Nomograms.align_residual — Tuple{Scale, Any, Tuple{Float64, Float64}, Tuple{Float64, Float64}}

source
align_residual(s::Scale, value, p, q) -> Float64

Signed area (twice the triangle area) of p, q and point(s, value). Zero exactly when the three are collinear, so this is the residual whose root evaluate seeks.

This is d'Ocagne's determinant criterion written as a cross product. Every layout in this package is a choice of scale curves that makes this criterion agree with the relation being charted.

Nomograms.auto_nomogram — Tuple{Any, Vararg{Any}}

source
auto_nomogram(F, x, y, [z]; kwargs...) -> Nomogram
auto_nomogram(F, x, y, w, ..., z; kwargs...) -> CompoundNomogram

Build a nomogram for z = F(x, y), or for z = F(x, y, w, …) with any number of inputs, from the function as you would normally write it.

The other constructors need you to rewrite your formula first, as f₁(u) + f₂(v) = f₃(w) with the transforms chosen and their inverses supplied. auto_nomogram does that for you:

nom = auto_nomogram((r, n) -> (1 + r/100)^n, :r => (1, 20), :n => (1, 30), :growth)

Give each input as a Variable with a domain, or as name => (lo, hi), in the order F takes them. The last argument names the result, as a Variable or a name. The exception is when there are exactly two arguments after F: both are then inputs, and the result is called :z. The result's domain is derived from F; if you give one, it is checked. Labels, tick formats and explicit ticks on the variables are used as given. Transforms are always derived, and replace any you set.

How

F can be drawn on three straight parallel scales exactly when transforms exist with h(z) = f(x) + g(y). Differentiating shows that requires

F_x / F_y = f′(x) / g′(y)

so the test is whether ln|F_x / F_y| splits into a part in x plus a part in y (see separability_defect). When it does, the same identity gives f′ and g′. Integrating them gives f and g, and h is found by walking across the level sets of F. The transforms come out as TabulatedScales, and each axis gets linear or decade-based ticks according to how its transform spreads its values.

Verification

The finished chart is then solved over a grid of inputs, and every answer is compared with F itself. If any answer differs by more than rtol times the result's range, no chart is returned: the call throws an error that reports the disagreement it measured. The chart's relation is F, so relation_residual and max_relation_residual check against your formula, not a derived one.

More than two inputs

One alignment line relates three scales, so F(x, y, w) is drawn as a chain of compound stages and the result is a CompoundNomogram. That needs F to depend on some pair of its inputs only through a single combination of them, t = T(x, y), which holds exactly when F_x / F_y does not depend on w. The first stage then charts t, the second charts z from t and w, and the reader pivots between them on an unmarked turning line. More inputs are reduced in the same way, one pair at a time.

Every pairing is tried. A pairing can fail for two reasons. F may not depend on the pair through a single combination at all. Or it may, but the two stages need the intermediate on different scales. The standard example is (x + y)·w, where the first stage needs a linear scale and the second a logarithmic one. That case needs a net, which you have to derive by hand (net_nomogram). If no pairing works, the error lists what went wrong with each.

One curved scale

A two-input formula that is not separable may still have a chart on two straight scales and one curve, such as the chart for the roots of a quadratic. Each variable is tried on the curve in turn. With G(u, v, w) = 0 and w on the curve, such a chart exists exactly when ln|G_u / G_v| splits as φ(w) + a(u) − b(v). This is tested by a least-squares fit along level sets of w. The derivatives it needs come from automatic differentiation of F itself, so F must accept generic number types as well as Float64. The first curved chart from a given function takes a few seconds to compile.

Formulas with no chart

A formula with neither kind of chart is refused, and the error gives the reason for each arrangement tried. The formula may still have a chart with more curved scales or a net, but you have to derive the form by hand: see determinant_nomogram and net_nomogram. A formula that is not monotone in a variable over its domain is also refused, because a straight scale cannot fold back on itself.

Keywords

  • width, height, title: as for the other constructions. width defaults to 70 for two inputs. In a chain it is the width of the last stage, and the default grows with the chain, 35 per input after the first. An earlier stage always has one input inside the rest of the chain, so it is mirrored and resized, to between a quarter of width and width, to put that input midway between the axes already there. Every way of pairing the inputs is tried for the first two levels of the chain, and the one whose axes end up furthest apart is kept.

  • rtol = 1e-6: the largest disagreement with F to accept, relative to the result's range.

Nomograms.axislabel — Tuple{Variable}

source
axislabel(v::Variable) -> String

The title drawn for a variable's axis: its label, followed by its unit in brackets when it has one and unitlabel is on.

Nomograms.candidate_ticks — Tuple{Variable, Tuple{Float64, Float64}}

source
candidate_ticks(v::Variable, domain) -> (majors, minors)

Tick values to try placing on the axis for variable v, honouring any explicit ticks or minorticks given on the variable and otherwise picking by tickstyle.

Nomograms.compound — Tuple{Vararg{Nomogram}}

source
compound(stages::Nomogram...; title = "", atol = 1e-6) -> CompoundNomogram

Join nomogram stages into one chart, positioning each so that the scales it shares with an earlier stage lie exactly on top of them.

Variables are shared by name. Each stage after the first must share at least one variable with a stage already placed, and that shared scale must have the same values over the same domain in both stages. So build the later stage with the domain that the earlier one derived:

first = additive_nomogram(a, b, Variable(:t))          # :t derived
second = additive_nomogram(Variable(:t; domain = first[:t].domain), c, d)
chart = compound(first, second)

The stages are aligned by a similarity (rotation, uniform scaling and translation) chosen to carry the later stage's shared scale onto the one already placed. The match is then checked along the whole domain, not only at the two points used to compute it. A similarity is affine, so it cannot change what any stage computes.

The shared scale is drawn once. The stages usually overlap instead of sitting side by side. A parallel-axis construction puts its result between its two inputs, so when two such stages share a pivot, the other scales of both stages lie on either side of it. Use project to reshape the result if the layout is awkward.

Nomograms.default_tickformat — Tuple{Real}

source
default_tickformat(v) -> String

Format a tick value compactly: integers without a decimal point, everything else to four significant figures.

Nomograms.determinant_nomogram — Tuple{Vararg{Pair}}

source
determinant_nomogram(var => curve, var => curve, var => curve, ...; kwargs...)

A nomogram from scales given directly as curves, fitted to the canvas for you.

This is the general constructor. Every three-variable nomogram is a set of parametric curves positioned so that d'Ocagne's determinant

| x₁(u)  y₁(u)  1 |
| x₂(v)  y₂(v)  1 |  =  0
| x₃(w)  y₃(w)  1 |

vanishes exactly when the relation holds. additive_nomogram and ratio_nomogram are two particular solutions of that condition. This constructor accepts any other solution you can write down, including curved and concurrent layouts that parallel axes cannot express.

Each argument is a Variable paired with a function from its value to a point. The curves can use whatever coordinates suit the geometry: their combined bounding box is measured and mapped onto width × height. An affine map keeps collinear points collinear, so the fit cannot change what the chart computes. This includes the default fit, which scales the two axes by different factors to fill the canvas.

Keywords

  • width, height: the canvas to fit to.

  • preserve_aspect: fit with one scale factor and centre the result, rather than stretching each axis to fill.

  • relation: the algebraic relation, if you have it, for relation_residual and max_relation_residual to check the geometry against. It is optional; the chart draws and solves without it.

  • title, nsamples: chart title, and how finely curves are sampled when measuring the bounding box.

Every variable must have a domain. There is no designated result variable, so no domain can be derived.

Example

The parallel-axis construction, written out by hand in its natural coordinates. Scales at x = 0, 2 and 1, with the middle one at half scale, encode u + v = w; the fit then stretches that 2 × 10 sketch onto the full canvas.

julia> nom = determinant_nomogram(
           Variable(:u; domain = (0, 10)) => u -> (0.0, u),
           Variable(:v; domain = (0, 10)) => v -> (2.0, v),
           Variable(:w; domain = (0, 20)) => w -> (1.0, w / 2);
           relation = (u, v, w) -> u + v - w,
       );

julia> round(evaluate(nom; u = 3, v = 4).w; digits = 10)
7.0

julia> point(nom[:v], 10)
(70.0, 100.0)

Nomograms.drawing — Tuple{AbstractNomogram}

source
drawing(nom::AbstractNomogram; style = NomogramStyle(), isopleths = Isopleth[])

Lay out a nomogram: choose and position ticks, resolve label collisions, and place titles and any alignment lines. Returns a NomogramDrawing of pure geometry, shared by every rendering backend.

Nomograms.evaluate — Tuple{CompoundNomogram}

source
evaluate(cn::CompoundNomogram; values...) -> NamedTuple

Solve a compound nomogram by reading its stages in turn.

Supply enough variables that some stage has exactly one unknown. That stage is solved, its answer is added to the known values, and the pass repeats until every variable is known. You can supply any set of variables that closes the chain. The intermediate on a shared scale is usually left to be derived, but it can also be an input.

Throws an error, naming the variables still unknown, if the values given are not enough.

Nomograms.evaluate — Tuple{NetNomogram}

source
evaluate(nn::NetNomogram; values...) -> NamedTuple

Solve a net chart. Supply three of its four variables.

How the chart is solved depends on which variable is unknown. If both net variables are known, the net gives a single point, and the problem is an ordinary three-point alignment. If one of them is unknown, the two scales already fix the line, and the answer is where that line crosses the strand for the known net variable. The strand is a curve, so the line may cross it more than once. As with other charts, evaluate throws an error when there are several answers, and evaluate_all returns them all.

Nomograms.evaluate — Tuple{Nomogram}

source
evaluate(nom::Nomogram; values...) -> NamedTuple

Solve the relation by alignment. Supply every variable but one, and the remaining one is read from its scale. The result is a NamedTuple of all the variables.

The solver only ever finds where a line crosses a scale, so forward and inverse problems use the same call: any one of the variables can be the unknown.

Throws an error if the alignment line crosses the unknown's scale more than once, which can only happen on a curved scale. Use evaluate_all to get every solution.

Example

julia> nom = additive_nomogram(
           Variable(:a; domain = (0, 10)),
           Variable(:b; domain = (0, 10)),
           Variable(:s),
       );

julia> round(evaluate(nom; a = 2, s = 9).b; digits = 10)
7.0

Nomograms.evaluate_all — Tuple{NetNomogram}

source
evaluate_all(nn::NetNomogram; values...) -> Vector{NamedTuple}

Every solution consistent with the three values given. See evaluate.

Nomograms.evaluate_all — Tuple{Nomogram}

source
evaluate_all(nom::Nomogram; values...) -> Vector{NamedTuple}

Every solution of the relation consistent with the values given, in increasing order of the unknown.

A straight alignment line can cross a curved scale more than once, and each crossing is a valid solution. evaluate throws an error in that case; evaluate_all returns all of them. For a parallel-layout nomogram the result always has exactly one element.

Example

julia> nom = additive_nomogram(
           Variable(:a; domain = (0, 10)),
           Variable(:b; domain = (0, 10)),
           Variable(:s),
       );

julia> length(evaluate_all(nom; a = 2, b = 3))
1

Nomograms.fan — Tuple{AbstractNomogram}

source
fan(nom; spread = 90, hub = 0.35, radius = 100, inward = :auto) -> nomogram

Redraw a chart with its axes as spokes radiating from one centre. The fanned chart gives exactly the same answers as the original. The result is the same kind of chart as nom: a Nomogram, a CompoundNomogram or a NetNomogram.

Parallel lines meet at a point at infinity, and a projective map can bring that point onto the page. Collinear points stay collinear under such a map, so the chart still works (see project). Every vertical axis becomes a spoke through the new point, including the turning lines of a compound chart and the strands of a net.

Straight lines stay straight, so the far ends of the spokes lie on a straight chord, not an arc, and the middle spokes are shorter than the outer ones; the wider the fan, the bigger the difference. At 90° the middle spokes' tips line up level with the outer spokes' tips.

The map also squeezes everything near the centre. So by default the chart is laid out both ways round, once with the top of each axis at the rim and once with it at the hub, and the layout that keeps more tick labels is used. A scale that is already crowded at one end, such as a reciprocal scale, works best with that end at the rim.

Keywords

  • spread = 90: the angle in degrees between the outermost spokes; less than 180.

  • hub = 0.35: where the spokes start, as a fraction of their length from the centre.

  • radius = 100: the length of the outer spokes, in chart units.

  • inward = :auto: true puts the top of each axis at the hub, false at the rim.

  • title: defaults to the chart's own.

fan works on any chart, but it is designed for parallel axes. A diagonal or curved scale is reshaped too and stays correct, but it does not become a spoke.

Nomograms.fraction_ticks — Tuple{Real, Real}

source
fraction_ticks(lo, hi; maxden = 16) -> (majors, minors)

Every fraction in [lo, hi] whose denominator divides maxden. Whole numbers come first (roundest first), then halves, quarters and so on. This is the order of priority on a crowded axis, so it keeps 1/2 before 3/8. Every fraction is also a minor, so one whose label is dropped keeps its mark, as on a ruler.

Nomograms.fractionformat — Union{Tuple{}, Tuple{Integer}}

source
fractionformat(maxden = 16; mixed = true) -> (value -> String)

A tick label format for fractions: 1 3/8 (or 11/8 with mixed = false), 3/4, 2. A value that is not a whole number of 1/maxdenths falls back to plain decimals, so an axis end at 0.3 still reads correctly. This is the default label format for tickstyle = :fraction; pass it as tickformat to label any axis this way.

Nomograms.frange — Tuple{ScaleTransform, Tuple{Real, Real}}

source
frange(t::ScaleTransform, domain) -> (lo, hi)

The interval spanned by transform(t, .) over domain, after checking that t is monotone there. A transform that folds over would make the axis double back on itself, which no layout can fix, so it is rejected early.

Nomograms.hasinverse — Tuple{ScaleTransform}

source
hasinverse(t::ScaleTransform) -> Bool

Whether t provides a closed-form invtransform.

Nomograms.invert — Tuple{ScaleTransform, Any, Tuple{Real, Real}}

source
invert(t::ScaleTransform, y, domain) -> Real

Solve transform(t, x) == y for x. Uses the closed-form inverse when available and falls back to bracketed root finding over domain = (lo, hi) otherwise.

Nomograms.invtransform

source
invtransform(t::ScaleTransform, y)

Map a functional-axis value back to the variable's own units. Only defined for transforms with hasinverse(t) == true.

Nomograms.isopleth — Tuple{CompoundNomogram, NamedTuple}

source
isopleth(cn::CompoundNomogram; label = "", values...) -> Vector{Isopleth}
isopleth(cn::CompoundNomogram, solution::NamedTuple; label = "") -> Vector{Isopleth}

The alignment lines for one reading of a compound nomogram: one per stage, since reading it means drawing a line across each in turn.

They come back sharing a group, so a renderer draws the whole reading in one colour. Pass the vector straight to drawing or a backend. The label goes on the stage where the answer is read: the one whose result scale no other stage shares. This is not always the last stage in the list, because auto_nomogram lists its final stage first, so list order is used only as a fallback.

Nomograms.isopleth — Tuple{NetNomogram, NamedTuple}

source
isopleth(nn::NetNomogram, solution::NamedTuple; label = "", group = 0) -> Isopleth
isopleth(nn::NetNomogram; label = "", group = 0, values...) -> Isopleth

The alignment line for one reading: it meets scale a, the net, and scale b. When the line crosses the known strand more than once, the keyword form returns that one line with every crossing marked, as for a Nomogram; see isopleth.

Nomograms.isopleth — Tuple{Nomogram, NamedTuple}

source
isopleth(nom::Nomogram; label = "", values...) -> Isopleth
isopleth(nom::Nomogram, solution::NamedTuple; label = "") -> Isopleth

The alignment line for one solution of the relation. It records where the line meets every scale, so that it can be drawn.

The first form solves for the missing variable. If the line crosses that variable's scale more than once (a curved scale can be crossed twice), every crossing is a solution and all of them lie on this one line. So the result is that line, with each crossing marked. Its values then hold only the values that fix the line, because the unknown has several values; get those from evaluate_all.

The second form takes a solution you already have, to draw one reading on its own:

isos = [isopleth(nom, sol; label = "z = $(sol.z)") for sol in evaluate_all(nom; p = -3, q = 2)]

Nomograms.isopleth_style — Tuple{Integer, Any, Any}

source
isopleth_style(i, colors, dashes) -> (color, dash)

Colour and dash for the i-th isopleth: the colour advances every line, the dash every full cycle of colours.

Nomograms.label_box — Tuple{Tuple{Float64, Float64}, AbstractString, Real, Tuple{Symbol, Symbol}, Real}

source
label_box(pos, text, size, align, rotation) -> (xmin, ymin, xmax, ymax)

The axis-aligned box a piece of text occupies, used both to size the canvas and to detect labels running into each other.

Nomograms.linear_ticks — Tuple{Real, Real}

source
linear_ticks(lo, hi; count = 6) -> (majors, minors)

Evenly spaced "nice" values covering [lo, hi], with each major interval subdivided five ways. The values are round and evenly spaced in the variable, not on the page. A nomogram axis is usually non-uniform, and thinning (see thin_ticks) decides what fits.

majors is returned roundest first, not sorted by value, because that is the order in which thin_ticks uses it.

Nomograms.log_ticks — Tuple{Real, Real}

source
log_ticks(lo, hi) -> (majors, minors)

Ticks for a logarithmic axis: every whole mantissa 1–9 in every decade the range touches, offered as a major, with tenths between them as minors.

There is no cut-off for wide ranges. majors is returned in the order thin_ticks should try them (decades, then 5s, then 2s, then the rest), and thinning keeps whatever fits. So a crowded axis ends up with decades only, a less crowded one with the familiar 1-2-5 sequence, and a roomy one labels every mantissa, all from the same candidates. A fixed rule that counts decades could pick only one of these, and an earlier rule of that kind labelled only the decades on an axis spanning just over two.

Every mantissa is also offered as a minor, so one whose label is dropped still gets a tick mark. On a range too narrow to hold three whole mantissas (100 to 200 holds only its ends), the tenths are promoted to majors as well, since otherwise almost nothing would be labelled.

Nomograms.max_relation_residual — Tuple{NetNomogram}

source
max_relation_residual(nn::NetNomogram; n = 7) -> Float64

The largest disagreement between a net chart's geometry and the relation it is meant to encode.

Sweeps both net variables over an n × n grid and the first scale over n values, solves the second scale geometrically, and checks each answer against the relation. Inputs whose alignment line misses a scale are skipped; NaN comes back when there is no relation to check or nothing in the sweep could be solved.

Nomograms.max_relation_residual — Tuple{Nomogram}

source
max_relation_residual(nom::Nomogram; n = 9) -> Float64

The largest disagreement between a nomogram's geometry and the relation it is meant to encode, over a grid of n × n inputs.

Getting the determinant form right is the hardest part of designing a nomogram by hand, and a chart with a small error in it still draws normally. This function solves for the third variable geometrically from the first two, then uses relation_residual to check whether the answer satisfies the relation. A result far from zero means the curves and the formula disagree.

Returns NaN for a nomogram built without a relation. Inputs whose alignment line misses the third scale are skipped, since they say nothing about correctness.

Nomograms.multiplicative_nomogram — Tuple{Variable, Variable, Variable}

source
multiplicative_nomogram(v1, v2, v3; width = 70, height = 100, title = "", max_modulus_ratio = 4)

A three-parallel-axis nomogram for the product relation

f1(v1) * f2(v2) = f3(v3)

Implemented by wrapping each variable's transform in a logarithm and deferring to additive_nomogram, which turns the product into a sum. All three transforms must be strictly positive over their domains. As with additive_nomogram, v3 is the result and may leave its domain to be derived.

The logarithm's base cancels out of the geometry, so none is offered.

Example

julia> nom = multiplicative_nomogram(
           Variable(:x; domain = (1, 10)),
           Variable(:y; domain = (1, 10)),
           Variable(:z),
       );

julia> nom[:z].domain
(1.0, 100.0)

julia> round(evaluate(nom; x = 2, y = 3).z; digits = 10)
6.0

Nomograms.net_nomogram — Tuple{Net, Pair, Pair}

source
net_nomogram(net::Net, a => curve_a, b => curve_b; kwargs...) -> NetNomogram

A four-variable chart: a Net carrying two variables, and two ordinary scales given as curves, in the manner of determinant_nomogram.

You can write everything in whatever coordinates suit the geometry, and it is all fitted to the canvas together. The fit is affine, so it keeps collinear points collinear and cannot change what the chart computes. The net's surface is moved by the same fit as the scales.

Keywords

Example

The general quadratic a·x² + b·x + c = 0. Written as a·x² + b·x = -c, the coefficients are α = x², β = x, γ = -c, so the net in (x, c) is

x₃ = x/(x² + x) = 1/(1 + x)        y₃ = -c/(x² + x) = -c/(x·(1 + x))
julia> nom = net_nomogram(
           Net(
               Variable(:x; domain = (0.5, 5)),
               Variable(:c; domain = (0, 4)),
               (x, c) -> (1 / (1 + x), -c / (x * (1 + x))),
           ),
           Variable(:a; domain = (0.5, 3)) => a -> (0.0, a),
           Variable(:b; domain = (-6, 0)) => b -> (1.0, b);
           relation = (x, c, a, b) -> a * x^2 + b * x + c,
       );

julia> [round(s.x; digits = 8) for s in evaluate_all(nom; a = 1, b = -3, c = 2)]
2-element Vector{Float64}:
 1.0
 2.0

Nomograms.netpoint — Tuple{NetNomogram, Any, Any}

source
netpoint(nn::NetNomogram, u, v) -> Point

Where the pair (u, v) falls on the chart's net.

Nomograms.nicestep — Tuple{Real}

source
nicestep(raw) -> Float64

Round raw up to the nearest "nice" tick interval: 1, 2, 2.5 or 5 times a power of ten.

Nomograms.nomogramplot! — Tuple

source
nomogramplot!(ax, nom::Nomogram; kwargs...)

Draw a nomogram into an existing Makie axis. See nomogramplot.

Nomograms.nomogramplot — Tuple

source
nomogramplot(nom::Nomogram; kwargs...) -> Figure
nomogramplot!(ax, nom::Nomogram; kwargs...) -> ax

Draw a nomogram with Makie. Available once a Makie backend is loaded:

using Nomograms, CairoMakie
fig = nomogramplot(nom; isopleths = [isopleth(nom; R1 = 4, R2 = 6)])
save("chart.pdf", fig)

Keywords

  • isopleths, style: as for drawing.

  • axiscolor, textcolor, isoplethcolors: colours.

  • pxperunit = 4.0: figure size per chart unit (nomogramplot only).

For vector output without a plotting dependency, use svg.

Nomograms.nudge_clear — Tuple{AbstractString, Tuple{Float64, Float64}, Tuple{Float64, Float64}, Real, Tuple{Symbol, Symbol}, NomogramStyle, Vector{NTuple{4, Float64}}}

source
nudge_clear(text, anchor, dir, size, align, style, occupied) -> (position, box)

Place a label at anchor, or at the nearest offset along ±dir that clears everything in occupied, and record the box it takes.

This is for text that should not be dropped: an axis title, or an isopleth label that the caller asked for explicitly. Tick labels are handled differently: there are many of them, so a tick whose label does not fit loses the label and keeps its mark.

Nomograms.point — Tuple{Scale, Any}

source
point(s::Scale, value) -> Point

Position of value on scale s, in chart coordinates.

Nomograms.project — Tuple{Nomogram, AbstractMatrix}

source
project(nom::Nomogram, H::AbstractMatrix; kwargs...) -> Nomogram
project(nom::Nomogram, src => dst; kwargs...) -> Nomogram

Reshape a nomogram with a projective map. The reshaped chart gives exactly the same answers as the original.

A projective map sends straight lines to straight lines, so it preserves the collinearity that a nomogram depends on. Everything else about the picture can change. For example, you can pull open a region where the ticks are crowded together, and the chart still computes exactly what it did before.

The second form builds the map from four point correspondences with projective_transform, which is usually the easiest way to specify it: give the corners of a quadrilateral in the current chart and where they should end up.

A projective map can move the bounding box anywhere, so the result is fitted to the canvas again. That fit is affine, and an affine map composed with a projective one is still projective, so the answers are still unchanged.

Keywords

  • width, height: canvas for the reshaped chart; defaults to the original's.

  • preserve_aspect, nsamples: as for determinant_nomogram.

  • title: defaults to the original's.

Example

julia> nom = additive_nomogram(
           Variable(:a; domain = (0, 10)),
           Variable(:b; domain = (0, 10)),
           Variable(:s),
       );

julia> skew = project(nom, [(0, 0), (70, 0), (70, 100), (0, 100)] =>
                           [(0, 0), (70, 20), (70, 80), (0, 100)]);

julia> round(evaluate(skew; a = 3, b = 4).s; digits = 10)
7.0

Nomograms.projective_transform — Tuple{Any, Any}

source
projective_transform(src, dst) -> Matrix{Float64}

The 3×3 homography taking the four points src to the four points dst, each given as a 4-tuple (or vector) of (x, y).

Four correspondences fix a projective map exactly, so this is a practical way to describe one: you say where four corners should land instead of writing out nine coefficients. No three of the four source points may be collinear, and likewise for the destination points.

Nomograms.ratio_nomogram — Tuple{Variable, Variable, Variable}

source
ratio_nomogram(v1, v2, v3; width = 70, height = 100, title = "")

An N-chart (also called a Z-chart) for the ratio relation

f1(v1) / f2(v2) = f3(v3)

It has two vertical scales joined by a diagonal, on which the result is read. v3 may leave domain = nothing, in which case the range it can reach is derived from the other two. All three transforms must be strictly positive over their domains.

Compared with a parallel chart

multiplicative_nomogram can also chart ratios, by taking logarithms. The N-chart needs no logarithms: the scales stay linear, so precision is even along them and small ratios are not squeezed into a corner. Use it for everyday quantities, such as a rate as distance over time, a concentration or a unit conversion, when the reader will want to interpolate by eye.

The construction

Put v1 on x = 0 at y1 = a1 * f1, running up from the bottom-left corner, and v2 on x = W at y2 = H - a2 * f2, running down from the top-right. Take the diagonal between those two corners, D(s) = (sW, sH). A line through a point on each outer scale meets the diagonal where

s = a1*f1 / (a1*f1 + a2*f2)

Writing r = f1/f2 and k = a1/a2 that is s = k*r / (k*r + 1), a one-to-one map from the ratio onto the diagonal: reading s back gives r = s / (k * (1 - s)).

The two scale factors do not have to be equal. The usual textbook derivation makes them equal so that the ratio comes out directly. Here the diagonal's own scale absorbs k instead, so each outer axis can be fitted to the canvas separately. Taking a1 = H/max(f1) and a2 = H/max(f2) anchors the diagonal at the two corners, which gives the layout its N shape.

The diagonal is drawn only over the part its scale reaches, f1/f2 between min(f1)/max(f2) and max(f1)/min(f2), so it stops short of the corners, which correspond to the ratios 0 and infinity.

Example

julia> nom = ratio_nomogram(
           Variable(:distance; domain = (1, 10)),
           Variable(:time; domain = (1, 10)),
           Variable(:speed),
       );

julia> nom[:speed].domain
(0.1, 10.0)

julia> round(evaluate(nom; distance = 6, time = 3).speed; digits = 10)
2.0

Nomograms.relation_residual — Tuple{Nomogram, NamedTuple}

source
relation_residual(nom::Nomogram, values::NamedTuple) -> Float64

The residual of the algebraic relation the nomogram was built from, independent of the geometry. Use it to check that a chart encodes the equation it is meant to: relation_residual should be zero for anything evaluate returns.

Returns NaN for nomograms built directly from scales, which carry no algebraic relation.

Nomograms.resolved_tickstyle — Tuple{Variable}

source
resolved_tickstyle(v::Variable) -> Symbol

:linear or :log, taking the variable's own tickstyle when it is not :auto and otherwise asking the transform.

Nomograms.save_svg — Tuple{AbstractString, Any}

source
save_svg(path, nom::AbstractNomogram; kwargs...) -> path
save_svg(path, d::NomogramDrawing; kwargs...) -> path

Write svg output to a file. Keywords are those of svg.

Nomograms.scale — Tuple{AbstractNomogram, Symbol}

source
scale(nom::AbstractNomogram, name::Symbol) -> Scale

The scale carrying the given variable name.

Nomograms.segment_hits_box — Tuple{Tuple{Float64, Float64}, Tuple{Float64, Float64}, NTuple{4, Float64}, Real}

source
segment_hits_box(a, b, box, pad) -> Bool

Whether the segment from a to b passes through box, grown by pad.

This uses the Liang–Barsky method: clip the segment against each of the box's four slabs in turn, and see whether any of it is left. It keeps labels clear of alignment lines, since a line drawn through a number makes it as hard to read as an overlapping label.

Nomograms.separability_defect — Tuple{Any, Any, Any}

source
separability_defect(F, xdomain, ydomain; n = 9) -> Float64

Measure how far z = F(x, y) is from being drawable on three straight parallel scales. The result is zero (up to rounding) when transforms f, g, h exist with h(z) = f(x) + g(y). Otherwise it is the largest violation found on an n × n grid.

This is the classical test: such transforms exist exactly when ln|F_x / F_y| splits into a function of x plus a function of y. A mixed second derivative of that is noisy to compute numerically, so this function checks an equivalent identity that needs only first derivatives:

L(x, y) − L(x, y₀) − L(x₀, y) + L(x₀, y₀) = 0

Throws an error if F is not monotone in each variable, because then no straight scale can represent it, whatever the transforms.

Nomograms.side_normals — Tuple{Tuple{Float64, Float64}}

source
side_normals(tangent) -> (left, right)

The two unit normals to a scale, named as Variable's side names them.

Nomograms.solve_all_on — Tuple{Scale, Tuple{Float64, Float64}, Tuple{Float64, Float64}}

source
solve_all_on(s::Scale, p, q) -> Vector{Float64}

Every value on scale s whose point lies on the line through p and q, in increasing order. Empty when the line misses the scale.

A straight line can cross a curved scale more than once, so there is not always a single solution. The usual shortcut, checking only the sign of the residual at the ends of the domain, would report no solution for an even number of crossings and pick one arbitrarily for an odd number. So this function searches the whole domain instead.

For a straight scale the residual is monotone and this returns at most one value, so charts with only straight scales never have more than one answer.

Nomograms.solve_on — Tuple{Scale, Tuple{Float64, Float64}, Tuple{Float64, Float64}}

source
solve_on(s::Scale, p, q) -> Float64

The single value on scale s whose point lies on the line through p and q.

Throws an error if the line misses the scale, or if it crosses the scale more than once, which can only happen on a curved scale. Use solve_all_on to get every crossing.

Nomograms.svg — Tuple{NomogramDrawing}

source
svg(nom::AbstractNomogram; kwargs...) -> String
svg(d::NomogramDrawing; kwargs...) -> String

Render a nomogram as a standalone SVG document, returned as a String.

This backend needs nothing beyond the standard library, so vector output works without any other packages. To put the chart in a Makie Figure, load Makie as well (see nomogramplot).

Keywords

  • isopleths, style: forwarded to drawing when given a Nomogram.

  • pxperunit = 4.0: pixels per chart unit; the only size control you normally need.

  • padding = 2.0: margin in chart units, added outside the computed bounding box.

  • background = "white", axiscolor, textcolor, isoplethcolors: colours, as any string an SVG renderer accepts. Pass background = "none" for a transparent chart.

  • gridcolor: the colour of a net's strands and their labels. By default they are drawn like the axes, since they are read like them; pass a lighter colour to make a dense net less prominent.

  • font: CSS font stack for all text.

Nomograms.thin_ticks — Tuple{Scale, AbstractVector{<:Real}, AbstractVector{<:Real}, NomogramStyle}

source
thin_ticks(s::Scale, majors, minors, style) -> Vector{TickMark}

Place as many of the candidate ticks as will fit, rejecting any that would land within min_major_spacing / min_minor_spacing of one already placed.

Thinning uses distance on the page, not distance in value. This keeps a reciprocal or logarithmic axis readable: the tick values stay round numbers, and where the axis is compressed some of them are dropped.

This function compares a scale only with itself. Collisions with labels from other scales are handled later, in one pass over the whole chart.

Nomograms.tickstyle — Tuple{ScaleTransform}

source
tickstyle(t::ScaleTransform) -> Symbol

How tick values should be chosen for a variable carrying this transform: :linear picks evenly spaced "nice" numbers, :log picks decades subdivided by mantissa.

Nomograms.transform

source
transform(t::ScaleTransform, x)

Map a value in the variable's own units onto the functional axis.

Nomograms.variablenames — Tuple{AbstractNomogram}

source
variablenames(nom::AbstractNomogram) -> Vector{Symbol}