Scales are a convenient abstraction for a fundamental task in visualization: mapping a dimension of abstract data to a visual representation. Although most often used for position-encoding quantitative data, such as mapping a measurement in meters to a position in pixels for dots in a scatterplot, scales can represent virtually any visual encoding, such as diverging colors, stroke widths, or symbol size. Scales can also be used with virtually any type of data, such as named categorical data or discrete data that requires sensible breaks.
For continuous quantitative data, you typically want a linear scale. (For time series data, a time scale.) If the distribution calls for it, consider transforming data using a log scale. A quantize scale may aid differentiation by rounding continuous data to a fixed set of discrete values.
For discrete ordinal (ordered) or categorical (unordered) data, an ordinal scale specifies an explicit mapping from a set of data values to a corresponding set of visual attributes (such as colors). The related band scale is useful for position-encoding ordinal data, such as bars in a bar chart.
Scales have no intrinsic visual representation. However, most scales can generate and format ticks for reference marks to aid in the construction of axes.
This API is highly polymorphic as each scale has different functions supported. This is still done in a convenient and type-safe manner, however the cost is a certain ugliness and complexity of the type signatures. For this reason after the type alias of each scale, the supported functions are listed along with a more specialized type signature appropriate for that scale type.
Note: As a convention, the scales typically take arguments in a range -> domain
order. This may seem somewhat counterinutive, as scales map a domain onto a range, but it is quite common to need to compute the domain, but know the range statically, so this argument order works much better for composition.
If you're new to this, I recommend ignoring the types of the type aliases and of the operations and just look at these listings.
Scale { domain : ( inp
, inp )
, range : ( Basics.Float
, Basics.Float )
, convert : ( inp
, inp ) -> ( Basics.Float
, Basics.Float ) -> inp -> Basics.Float
, invert : ( inp
, inp ) -> ( Basics.Float
, Basics.Float ) -> Basics.Float -> inp
, ticks : ( inp
, inp ) -> Basics.Int -> List inp
, tickFormat : ( inp
, inp ) -> Basics.Int -> inp -> String
, nice : ( inp
, inp ) -> Basics.Int -> ( inp
, inp )
, rangeExtent : ( inp
, inp ) -> ( Basics.Float
, Basics.Float ) -> ( Basics.Float
, Basics.Float )
}
Maps a (inp, inp)
domain to a
(Float, Float)
range (this will be either (Float, Float)
or (Time.Posix, Time.Posix)
.)
Continuous scales support the following operations:
convert : ContinuousScale inp -> inp -> Float
invert : ContinuousScale inp -> Float -> inp
domain : ContinuousScale inp -> (inp, inp)
range : ContinuousScale inp -> (Float, Float)
rangeExtent : ContinuousScale inp -> (Float, Float)
(which is in this case just an alias for range
)ticks : ContinuousScale inp -> Int -> List inp
tickFormat : ContinuousScale inp -> Int -> inp -> String
clamp : ContinuousScale inp -> ContinuousScale inp
nice : Int -> ContinuousScale inp -> ContinuousScale inp
linear : ( Basics.Float, Basics.Float ) -> ( Basics.Float, Basics.Float ) -> ContinuousScale Basics.Float
Linear scales are a good default choice for continuous quantitative data because they preserve proportional differences. Each range value y can be expressed as a function of the domain value x: y = mx + b.
scale : ContinuousScale
scale = Scale.linear ( 50, 100 ) ( 0, 1 )
Scale.convert scale 0.5 --> 75
power : Basics.Float -> ( Basics.Float, Basics.Float ) -> ( Basics.Float, Basics.Float ) -> ContinuousScale Basics.Float
Power scales are similar to linear scales, except an exponential transform is applied to the input domain value before the output range value is computed. Each range value y can be expressed as a function of the domain value x: y = mx^k + b, where k is the exponent value. Power scales also support negative domain values, in which case the input value and the resulting output value are multiplied by -1.
The arguments are exponent
, range
and domain
scale : ContinuousScale
scale = power 2 ( 0, 1 ) ( 50, 100 )
convert scale 0.5 == 62.5
log : Basics.Float -> ( Basics.Float, Basics.Float ) -> ( Basics.Float, Basics.Float ) -> ContinuousScale Basics.Float
Log scales are similar to linear scales, except a logarithmic transform is applied to the input domain value before the output range value is computed. The mapping to the range value y can be expressed as a function of the domain value x: y = m log(x) + b.
As log(0) = -∞, a log scale domain must be strictly-positive or strictly-negative; the domain must not include or cross zero. A log scale with a positive domain has a well-defined behavior for positive values, and a log scale with a negative domain has a well-defined behavior for negative values. (For a negative domain, input and output values are implicitly multiplied by -1.) The behavior of the scale is undefined if you pass a negative value to a log scale with a positive domain or vice versa.
The arguments are base
, range
, and domain
.
scale : ContinuousScale
scale = log 10 ( 10, 1000 ) ( 50, 100 )
convert scale 100 --> 75
symlog : Basics.Float -> ( Basics.Float, Basics.Float ) -> ( Basics.Float, Basics.Float ) -> ContinuousScale Basics.Float
The symlog scale is similar to a log scale in that is suitable for showing values with large and small quantities at the same time. However it also allows visualizing positive and negative quantities at the same time (as well as zero) with a smooth transform.
This is controlled with a parameter. A good default value is 1 / logBase e 10
- this corresponds
to a linear scale around zero.
For more background, see A bi-symmetric log transformation for wide-range data by Weber.
identity : ( Basics.Float, Basics.Float ) -> ContinuousScale Basics.Float
Identity scales are a special case of linear scales where the domain and range are identical; the convert and invert operations are thus the identity function. These scales are occasionally useful when working with pixel coordinates, say in conjunction with an axis.
time : Time.Zone -> ( Basics.Float, Basics.Float ) -> ( Time.Posix, Time.Posix ) -> ContinuousScale Time.Posix
Time scales are a variant of linear scales that have a temporal domain: domain values are times rather than floats, and invert likewise returns a time. Time scales implement ticks based on calendar intervals, taking the pain out of generating axes for temporal domains.
Since time scales use human time to calculate ticks and display ticks, we need the time zone that you will want to display your data in.
radial : ( Basics.Float, Basics.Float ) -> ( Basics.Float, Basics.Float ) -> ContinuousScale Basics.Float
Radial scales are a variant of linear scales where the range is internally squared so that an input value corresponds linearly to the squared output value. These scales are useful when you want the input value to correspond to the area of a graphical mark and the mark is specified by radius, as in a radial bar chart.
Sequential scales are similar to continuous scales in that they map a continuous, numeric input domain to a continuous output range. However, unlike continuous scales, the output range of a sequential scale is fixed by its interpolator function.
Scale { domain : ( Basics.Float
, Basics.Float )
, range : Basics.Float -> a
, convert : ( Basics.Float
, Basics.Float ) -> (Basics.Float -> a) -> Basics.Float -> a
}
This transforms a continuous (Float, Float)
domain to an arbitrary range a
defined by the interpolator function Float -> a
, where the Float
goes from 0 to 1.
Sequential scales support the following operations:
convert : SequentialScale a -> Float -> a
domain : SequentialScale a -> (Float, Float)
range : SequentialScale a -> Float -> a
Sequential scales can easily be used with Interpolators.
sequential : (Basics.Float -> a) -> ( Basics.Float, Basics.Float ) -> SequentialScale a
Construct a sequential scale.
sequentialLog : Basics.Float -> (Basics.Float -> a) -> ( Basics.Float, Basics.Float ) -> SequentialScale a
A sequential scale with a logarithmic transform.
sequentialSymlog : Basics.Float -> (Basics.Float -> a) -> ( Basics.Float, Basics.Float ) -> SequentialScale a
A sequential scale with a syslog transform.
Diverging scales, like sequential scales, are similar to continuous scales in that they map a continuous, numeric input domain to a continuous output range. However, unlike continuous scales, the input domain and output range of a diverging scale always has exactly three elements, and the output range is specified as an interpolator rather than an array of values. These scales do not expose invert and interpolate methods.
Scale { domain : ( Basics.Float
, Basics.Float
, Basics.Float )
, range : Basics.Float -> a
, convert : ( Basics.Float
, Basics.Float
, Basics.Float ) -> (Basics.Float -> a) -> Basics.Float -> a
}
This transforms a continuous (Float, Float, Float)
domain to an arbitrary range a
defined by the interpolator function Float -> a
, where the Float
goes from 0 to 1.
The middle float is the neutral or zero point.
Diverging scales support the following operations:
convert : DivergingScale a -> Float -> a
domain : DivergingScale a -> (Float, Float)
range : DivergingScale a -> Float -> a
diverging : (Basics.Float -> a) -> ( Basics.Float, Basics.Float, Basics.Float ) -> DivergingScale a
Construct a diverging scale.
Note that if you'd rather specify the interpolator also as a triple, you can do the following:
import Interpolation exposing (DivergingScale)
import Scale
makeDiverging : ( Float, Float, Float ) -> ( Float, Float, Float ) -> DivergingScale Float
makeDiverging ( r0, r1, r2 ) domain =
Scale.diverging (Interpolation.piecewise Interpolation.float r0 [ r1, r2 ]) domain
You can adapt this to any type by replacing Interpolation.float
with an appropriate interpolator.
divergingLog : Basics.Float -> (Basics.Float -> a) -> ( Basics.Float, Basics.Float, Basics.Float ) -> DivergingScale a
A diverging scale with a logarithmic transform.
divergingSymlog : Basics.Float -> (Basics.Float -> a) -> ( Basics.Float, Basics.Float, Basics.Float ) -> DivergingScale a
A diverging scale with a syslog transform.
divergingPower : Basics.Float -> (Basics.Float -> a) -> ( Basics.Float, Basics.Float, Basics.Float ) -> DivergingScale a
A diverging scale with a power transform.
Quantize scales are similar to linear scales, except they use a discrete rather
than continuous range. The continuous input domain is divided into uniform
segments based on the number of values in (i.e., the cardinality of) the output
range. Each range value y can be expressed as a quantized linear function of the
domain value x
: y = m round(x) + b
.
Scale { domain : ( Basics.Float
, Basics.Float )
, range : ( a
, List a )
, convert : ( Basics.Float
, Basics.Float ) -> ( a
, List a ) -> Basics.Float -> a
, invertExtent : ( Basics.Float
, Basics.Float ) -> ( a
, List a ) -> a -> Maybe ( Basics.Float
, Basics.Float )
, ticks : ( Basics.Float
, Basics.Float ) -> ( a
, List a ) -> Basics.Int -> List Basics.Float
, tickFormat : ( Basics.Float
, Basics.Float ) -> Basics.Int -> Basics.Float -> String
, nice : ( Basics.Float
, Basics.Float ) -> Basics.Int -> ( Basics.Float
, Basics.Float )
, rangeExtent : ( Basics.Float
, Basics.Float ) -> ( a
, List a ) -> ( a
, a )
}
These transform a (Float, Float)
domain
to an arbitrary non-empty list (a, List a)
.
Quantize scales support the following operations:
convert : QuantizeScale a -> Float -> a
,invertExtent : QuantizeScale a -> a -> Maybe (Float, Float)
domain : QuantizeScale a -> (Float, Float)
range : QuantizeScale a -> (a, List a)
,rangeExtent : QuantizeScale a -> (a, a)
ticks : QuantizeScale a -> Int -> List Float
tickFormat : QuantizeScale a -> Int -> Float -> String
nice : Int -> QuantizeScale a -> QuantizeScale a
clamp : QuantizeScale a -> QuantizeScale a
quantize : ( a, List a ) -> ( Basics.Float, Basics.Float ) -> QuantizeScale a
Constructs a new quantize scale. The range for these is a
non-empty list represented as a (head, tail)
tuple.
Quantile scales map a sampled input domain to a discrete range. The number of values
in the output range determines the number of quantiles that will be computed from the domain.
To compute the quantiles, the domain is sorted, and treated as a population of discrete values;
see Statistics.quantile
.
Scale { domain : List Basics.Float
, range : Array a
, convert : List Basics.Float -> Array a -> Basics.Float -> a
, invertExtent : List Basics.Float -> Array a -> a -> Maybe ( Basics.Float
, Basics.Float )
, quantiles : List Basics.Float
}
These transform a List Float
domain
to an arbitrary non-empty list (a, List a)
. However, internally this gets converted to a sorted Array.
Quantile scales support the following operations:
convert : QuantileScale a -> Float -> a
invertExtent : QuantileScale a -> a -> Maybe (Float, Float)
domain : QuantileScale a -> List Float
range : QuantileScale a -> Array a
quantiles : QuantileScale a -> List Float
quantile : ( a, List a ) -> List Basics.Float -> QuantileScale a
Constructs a new quantile scale. The range must be non-empty and is represented as a ( head, tail )
tuple.
Threshold scales are similar to quantize scales, except they allow you to map arbitrary subsets of the domain to discrete values in the range. The input domain is still continuous, and divided into slices based on a set of threshold values.
Scale { domain : Array comparable
, range : Array a
, convert : Array comparable -> Array a -> comparable -> a
}
These transform a Array comparable
domain to an arbitrary Array a
.
Threshold scales support the following operations:
convert : ThresholdScale comparable a -> comparable -> a
domain : ThresholdScale comparable a -> Array comparable
range : ThresholdScale comparable a -> Array a
threshold : ( a, List ( comparable, a ) ) -> ThresholdScale comparable a
Constructs a threshold scale. The signature here is a bit different than other scales as it is designed to reinforce that the thresholds seperate the domain values.
Hence: temperatureScale = threshold ( blue, [ ( 0, yellow ), ( 200, red )])
intuitavely shows
that temperatures lower than 0
will be blue
, between 0
and 200
will be yello
and above
will be red
. It also neatly avoids any questions of what happens if there are more than expected
of either domain
or range
values, as this is impossible by construction.
However, if you would like to use the traditional separate domain
and range
lists, you can
make use of the following function, which simply ignores extra elements:
interleave : ( a, List a ) -> List comparable -> ( a, List ( comparable, a ) )
interleave ( r, range ) domain =
( r, List.map2 Tuple.pair domain, range )
You could of course make a variation that does some error handling if the lists don't match.
Unlike continuous scales, ordinal scales have a discrete domain and range. For example, an ordinal scale might map a set of named categories to a set of colors, or determine the horizontal positions of columns in a column chart.
Scale { domain : List a
, range : List b
, convert : List a -> List b -> a -> Maybe b
}
Type alias for ordinal scales. These transform an arbitrary
List a
domain to an arbitrary list List b
, where the mapping
is based on order.
Ordinal scales support the following operations:
convert : OrdinalScale a b -> a -> Maybe b
Note that this returns a Maybe
value in the case when you pass a value that isn't in the domain.
Band scales are like ordinal scales except the output range is continuous and numeric. Discrete output values are automatically computed by the scale by dividing the continuous range into uniform bands. Band scales are typically used for bar charts with an ordinal or categorical dimension.
Scale { domain : List a
, range : ( Basics.Float
, Basics.Float )
, convert : List a -> ( Basics.Float
, Basics.Float ) -> a -> Basics.Float
, bandwidth : Basics.Float
}
Type alias for a band scale. These transform an arbitrary List a
to a continous (Float, Float) by uniformely partitioning the range.
Band scales support the following operations:
convert : BandScale a -> a -> Float
domain : BandScale a -> List a
range : Bandscale a -> (Float, Float)
bandwidth : Bandscale a -> Float
toRenderable : (a -> String) -> BandScale a -> RenderableScale a
band : BandConfig -> ( Basics.Float, Basics.Float ) -> List a -> BandScale a
Constructs a band scale.
{ paddingInner : Basics.Float
, paddingOuter : Basics.Float
, align : Basics.Float
}
Configuration options for deciding how bands are partioned,
.paddingInner : Float
The inner padding determines the ratio (so the value must be in the range [0, 1]) of the range that is reserved for blank space between bands.
.paddingOuter : Float
The outer padding determines the ratio (so the value must be in the range [0, 1]) of the range that is reserved for blank space before the first band and after the last band.
.align : Float
The alignment determines how any leftover unused space in the range is distributed. A value of 0.5 indicates that the leftover space should be equally distributed before the first band and after the last band; i.e., the bands should be centered within the range. A value of 0 or 1 may be used to shift the bands to one side, say to position them adjacent to an axis.
defaultBandConfig : BandConfig
Creates some reasonable defaults for a BandConfig:
defaultBandConfig --> { paddingInner = 0.0, paddingOuter = 0.0, align = 0.5 }
Point scales are a variant of band scales with the bandwidth fixed to zero. Point scales are typically used for scatterplots with an ordinal or categorical dimension.
point : PointConfig -> ( Basics.Float, Basics.Float ) -> List a -> BandScale a
Constructs a point scale.
{ padding : Basics.Float
, align : Basics.Float
}
Configuration options for Point scales. See BandConfig for details, as align
works exactly the same, and padding
is equivalent to paddingOuter
.
defaultPointConfig : PointConfig
Creates some reasonable defaults for a PointConfig:
defaultPointConfig --> { padding = 0.0, align = 0.5 }
These functions take Scales and do something with them. Check the docs of each scale type to see which operations it supports.
convert : Scale { a | convert : domain -> range -> value -> result, domain : domain, range : range } -> value -> result
Given a value from the domain, returns the corresponding value from the range. If the given value is outside the domain the mapping may be extrapolated such that the returned value is outside the range.
invert : Scale { a | invert : domain -> range -> value -> result, domain : domain, range : range } -> value -> result
Given a value from the range, returns the corresponding value from the domain. Inversion is useful for interaction, say to determine the data value corresponding to the position of the mouse.
invertExtent : Scale { a | invertExtent : domain -> range -> value -> Maybe ( comparable, comparable ), domain : domain, range : range } -> value -> Maybe ( comparable, comparable )
Returns the extent of values in the domain for the corresponding value in the range. This method is useful for interaction, say to determine the value in the domain that corresponds to the pixel location under the mouse.
domain : Scale { a | domain : domain } -> domain
Retrieve the domain of the scale.
range : Scale { a | range : range } -> range
Retrieve the range of the scale.
rangeExtent : Scale { a | rangeExtent : domain -> range -> ( b, b ), domain : domain, range : range } -> ( b, b )
Retrieve the minimum and maximum elements from the range.
ticks : Scale { a | ticks : domain -> Basics.Int -> List ticks, domain : domain } -> Basics.Int -> List ticks
The second argument controls approximately how many representative values from the scale’s domain to return. A good default value is 10. The returned tick values are uniformly spaced, have human-readable values (such as multiples of powers of 10), and are guaranteed to be within the extent of the domain. Ticks are often used to display reference lines, or tick marks, in conjunction with the visualized data. The specified count is only a hint; the scale may return more or fewer values depending on the domain.
scale : ContinuousScale Float
scale = linear ( 10, 100 ) ( 50, 100 )
ticks scale 10 --> [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
tickFormat : Scale { a | tickFormat : domain -> Basics.Int -> value -> String, domain : domain, convert : domain -> range -> value -> b } -> Basics.Int -> value -> String
A number format function suitable for displaying a tick value, automatically computing the appropriate precision based on the fixed interval between tick values. The specified count should have the same value as the count that is used to generate the tick values.
clamp : Scale { a | convert : ( Basics.Float, Basics.Float ) -> range -> Basics.Float -> result } -> Scale { a | convert : ( Basics.Float, Basics.Float ) -> range -> Basics.Float -> result }
Enables clamping on the domain, meaning the return value of the scale is always within the scale’s range.
scale : ContinuousScale Float
scale = Scale.linear ( 50, 100 ) ( 10, 100 )
Scale.convert scale 1 --> 45
Scale.convert (Scale.clamp scale) 1 --> 50
nice : Basics.Int -> Scale { a | nice : domain -> Basics.Int -> domain, domain : domain } -> Scale { a | nice : domain -> Basics.Int -> domain, domain : domain }
Returns a new scale which extends the domain so that it lands on round values. The first argument is the same as you would pass to ticks.
scale : ContinuousScale Float
scale = Scale.linear ( 0.5, 99 ) ( 50, 100 )
Scale.domain (Scale.nice 10 scale) --> (0, 100)
quantiles : Scale { a | quantiles : b } -> b
Returns the quantile thresholds. If the range contains n
discrete values, the returned list will contain n - 1
thresholds. Values less than the first threshold are considered in the first quantile; values greater than or equal to the first threshold but less than the second threshold are in the second quantile, and so on.
bandwidth : Scale { scale | bandwidth : Basics.Float } -> Basics.Float
Returns the width of a band in a band scale.
scale : BandScale String
scale = Scale.band Scale.defaultBandConfig (0, 120) ["a", "b", "c"]
Scale.bandwidth scale --> 40
toRenderable : (a -> String) -> BandScale a -> Scale { ticks : List a -> Basics.Int -> List a, domain : List a, tickFormat : List a -> Basics.Int -> a -> String, convert : List a -> ( Basics.Float, Basics.Float ) -> a -> Basics.Float, range : ( Basics.Float, Basics.Float ), rangeExtent : List a -> ( Basics.Float, Basics.Float ) -> ( Basics.Float, Basics.Float ) }
This converts a BandScale into a RenderableScale
suitable for rendering Axes. This has the same domain and range, but the convert output is shifted by half a bandwidth
in order for ticks and labels to align nicely.