io.data2viz.scale.Ordinal.kt Maven / Gradle / Ivy
package io.data2viz.scale
/**
* 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.
*
*
*/
class IndexableDomain : DiscreteDomain{
internal val index: MutableMap = HashMap()
internal val _domain: MutableList = arrayListOf()
/**
* The first element in domain will be mapped to the first element in the range, the second domain value to
* the second range value, and so on.
*
* Domain values are stored internally in a map from stringified value to index; the resulting index is then used
* to retrieve a value from the range. Thus, an ordinal scale’s values must be coercible to a string,
* and the stringified version of the domain value uniquely identifies the corresponding range value.
*
* Setting the domain on an ordinal scale is optional if the unknown value is implicit (the default).
* In this case, the domain will be inferred implicitly from usage by assigning each unique value passed
* to the scale a new value from the range.
*
* Note that an explicit domain is recommended to ensure deterministic behavior, as inferring the domain
* from usage will be dependent on ordering.
*/
override var domain: List
get() = _domain.toList()
set(value) {
_domain.clear()
index.clear()
value.forEach {
if (!index.containsKey(it)) {
_domain.add(it)
index.put(it, _domain.size - 1)
}
}
}
}
open class OrdinalScale(range: List = listOf(), val indexableDomain: IndexableDomain = IndexableDomain()) : Scale, DiscreteDomain by indexableDomain {
protected val _range: MutableList = arrayListOf()
init {
_range.addAll(range.toList())
}
/**
* If range is specified, sets the range of the ordinal scale to the specified array of values.
* The first element in the domain will be mapped to the first element in range, the second domain value to
* the second range value, and so on.
* If there are fewer elements in the range than in the domain, the scale will reuse values from the start
* of the range.
*/
var range: List
get() = _range.toList()
set(value) {
require(value.isNotEmpty(), { "Range can't be empty." })
_range.clear()
_range.addAll(value.toList())
}
/**
* The behavior when asking for a rangeValue with a given non-existant domainValue :
* If unknown is null : add domainValue to the domain, then return a rangeValue (= scale implicit).
* If unknown is not null : return unknown.
*/
// TODO : change behavior
private var _unknown: R? = null
var unknown: R?
get() = _unknown
set(value) {
_unknown = value
}
override operator fun invoke(domainValue: D): R {
if (_unknown == null && !indexableDomain.index.containsKey(domainValue)) {
indexableDomain._domain.add(domainValue)
indexableDomain.index.put(domainValue, indexableDomain._domain.size - 1)
}
val index = indexableDomain.index[domainValue] ?: return _unknown ?: throw IllegalStateException()
return when {
_range.isEmpty() -> _unknown ?: throw IllegalStateException()
else -> _range[index % _range.size]
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy