///|
/// Live string-unit mapping shared by installed category tickers and axes.
/// The separate ticker/category convenience converter remains persistent.
pub struct CategoryUnits {
priv labels : Array[String]
}
///|
pub fn CategoryUnits::new(labels : Array[String]) -> CategoryUnits {
let result : CategoryUnits = { labels: [], }
ignore(result.encode(labels))
result
}
///|
pub fn CategoryUnits::labels(self : CategoryUnits) -> Array[String] {
self.labels.copy()
}
///|
pub fn CategoryUnits::positions(self : CategoryUnits) -> Array[Double] {
Array::makei(self.labels.length(), index => index.to_double())
}
///|
pub fn CategoryUnits::encode(
self : CategoryUnits,
values : Array[String],
) -> Array[Double] {
values.map(value => {
let mut found : Int32? = None
for index, label in self.labels {
if label == value {
found = Some(index)
break
}
}
let index = match found {
Some(index) => index
None => {
let index = self.labels.length()
self.labels.push(value)
index
}
}
index.to_double()
})
}
///|
pub fn CategoryUnits::format_values(
self : CategoryUnits,
values : Array[Double],
) -> Array[String] raise @core.DataError {
values.map(value => {
if value.is_nan() {
raise @core.DataError::InvalidTicks(
message="cannot convert float NaN to integer",
)
}
if value.is_inf() {
raise @core.DataError::InvalidTicks(
message="cannot convert float infinity to integer",
)
}
if value < -1.0 || value > self.labels.length().to_double() + 1.0 {
""
} else {
let lower = value.floor()
let fraction = value - lower
let rounded = lower +
(if fraction > 0.5 || (fraction == 0.5 && lower % 2.0 != 0.0) {
1.0
} else {
0.0
})
if rounded >= 0.0 && rounded < self.labels.length().to_double() {
self.labels[rounded.to_int()]
} else {
""
}
}
})
}
///|
pub(all) enum AxisUnitTag {
Date(@date.DateTimeZone)
Category(CategoryUnits)
}
///|
priv enum AxisUnitConverter {
DefaultDate
ExplicitDate(@date.DateConverter)
Category
}
///|
priv struct AxisUnitState {
mut converter : AxisUnitConverter?
mut explicit : Bool
mut unit : AxisUnitTag?
mut group : (() -> Array[Axis])?
mut data_mutated : (() -> Bool)?
mut changed : (() -> Unit raise @core.DataError)?
}
///|
fn AxisUnitState::new() -> AxisUnitState {
{
converter: None,
explicit: false,
unit: None,
group: None,
data_mutated: None,
changed: None,
}
}
///|
fn Axis::reset_unit_state(self : Axis) -> Unit {
self.units_state.converter = None
self.units_state.explicit = false
self.units_state.unit = None
}
///|
pub fn Axis::bind_unit_group(self : Axis, provider : () -> Array[Axis]) -> Unit {
self.units_state.group = Some(provider)
}
///|
pub fn Axis::bind_unit_data_mutated(self : Axis, provider : () -> Bool) -> Unit {
self.units_state.data_mutated = Some(provider)
}
///|
pub fn Axis::bind_units_changed(
self : Axis,
provider : () -> Unit raise @core.DataError,
) -> Unit {
self.units_state.changed = Some(provider)
}
///|
/// Converter presence and unit presence are independent on shared axes.
pub fn Axis::have_units(self : Axis) -> Bool {
self.units_state.converter is Some(_) || self.units_state.unit is Some(_)
}
///|
pub fn Axis::unit_tag(self : Axis) -> AxisUnitTag? {
self.units_state.unit
}
///|
pub fn Axis::unit_converter_explicit(self : Axis) -> Bool {
self.units_state.explicit
}
///|
pub fn Axis::unit_converter_kind(self : Axis) -> String? {
self.units_state.converter.map(value => {
match value {
DefaultDate => "default_date"
ExplicitDate(_) => "explicit_date"
Category => "category"
}
})
}
///|
pub fn Axis::get_category_units(self : Axis) -> CategoryUnits? {
match self.units_state.unit {
Some(Category(value)) => Some(value)
_ => None
}
}
///|
fn same_unit_tag(left : AxisUnitTag?, right : AxisUnitTag?) -> Bool {
match (left, right) {
(None, None) => true
(Some(Date(a)), Some(Date(b))) => a.unit_equal(b)
(Some(Category(a)), Some(Category(b))) => physical_equal(a, b)
_ => false
}
}
///|
fn Axis::units_date_converter(self : Axis) -> @date.DateConverter? {
match self.units_state.converter {
Some(DefaultDate) => Some(@date.default_date_converter())
Some(ExplicitDate(value)) => Some(value)
_ => None
}
}
///|
fn Axis::units_date_info(
self : Axis,
converter : @date.DateConverter,
) -> @date.DateAxisInfo raise @core.DataError {
let zone = match self.units_state.unit {
None => None
Some(Date(zone)) => Some(zone)
Some(Category(_)) =>
raise @core.DataError::InvalidTicks(
message="date converter requires a date timezone unit",
)
}
self.checked_date_axisinfo(converter, unit?=zone)
}
///|
/// Source set_converter assigns and marks explicit without updating axis info.
pub fn Axis::set_explicit_date_converter(
self : Axis,
converter : @date.DateConverter,
) -> Unit raise @core.DataError {
if self.units_state.explicit {
if self.units_state.converter is Some(ExplicitDate(current)) &&
physical_equal(current, converter) {
return
}
raise @core.DataError::InvalidTicks(
message="Axis already has an explicit converter set",
)
}
self.units_state.converter = Some(ExplicitDate(converter))
self.units_state.explicit = true
}
///|
/// Reconsider converter defaults role by role, retaining explicit user slots.
pub fn Axis::update_unit_axisinfo(self : Axis) -> Unit raise @core.DataError {
match self.units_state.converter {
None => return
Some(DefaultDate) | Some(ExplicitDate(_)) => {
let converter = self.units_date_converter().unwrap()
let info = self.units_date_info(converter)
self.date_converter = Some(converter)
if self.date_format is None {
self.date_format = Some("")
}
self.date_timezone = info.major_locator().timezone()
self.date_offset_minutes = info.major_locator().offset_minutes()
self.date_timezone_is_default = self.units_state.unit is None
self.date_timezone_override = false
self.install_date_unit_defaults(info)
}
Some(Category) => {
let units = match self.get_category_units() {
Some(value) => value
None =>
raise @core.DataError::InvalidTicks(
message="categorical converter requires category units",
)
}
self.install_category_unit_defaults(units)
}
}
self.set_unit_default_intervals()
}
///|
fn Axis::set_unit_default_intervals(self : Axis) -> Unit raise @core.DataError {
if self.units_state.data_mutated is Some(provider) && provider() {
return
}
let (first, last) = self.lifecycle_view_interval_checked()
if first != 0.0 || last != 1.0 {
return
}
if self.units_date_converter() is Some(converter) {
// The source asks axisinfo a second time at the default-interval stage.
let info = self.units_date_info(converter)
let (first, last) = info.default_limits()
let view = @core.limits(first.to_ordinal(), last.to_ordinal())
self.stored_view = view
self.inverted = view.lower() > view.upper()
if self.autoscale_enabled {
self.limits = None
self.auto_view = Some(view)
} else {
self.limits = Some(view)
self.auto_view = None
}
if self.tick_layout_view is Some(_) {
self.tick_layout_view = Some((view.lower(), view.upper()))
}
}
}
///|
/// Assign each sibling before info/callbacks; failures leave preceding changes.
pub fn Axis::set_unit_tag(
self : Axis,
unit : AxisUnitTag?,
) -> Unit raise @core.DataError {
if same_unit_tag(self.units_state.unit, unit) {
return
}
let axes = match self.units_state.group {
Some(provider) => provider()
None => [self]
}
for axis in axes {
axis.units_state.unit = unit
axis.update_unit_axisinfo()
if axis.units_state.changed is Some(provider) {
provider()
}
}
}
///|
/// Select the implicit date converter from explicit naive/aware input.
pub fn Axis::update_date_units(
self : Axis,
values : Array[@date.RRuleDateTime],
) -> Bool raise @core.DataError {
if values.is_empty() && !self.units_state.explicit {
return false
}
if self.units_state.explicit && self.units_state.converter is Some(Category) {
if !values.is_empty() {
raise @core.DataError::InvalidTicks(
message="'value' must be an instance of str or bytes, not a datetime.datetime",
)
}
if self.units_state.unit is None {
self.set_unit_tag(Some(AxisUnitTag::Category(CategoryUnits::new([]))))
}
return true
}
let need_new = if self.units_state.explicit {
false
} else {
let changed = !(self.units_state.converter is Some(DefaultDate))
self.units_state.converter = Some(DefaultDate)
changed
}
let unit = if values.is_empty() {
None
} else {
values[0].timezone().map(zone => AxisUnitTag::Date(zone))
}
if unit is Some(_) && self.units_state.unit is None {
self.set_unit_tag(unit)
} else if need_new {
self.update_unit_axisinfo()
}
true
}
///|
/// Select/update live string units, without resetting installed user tickers.
pub fn Axis::update_category_units(
self : Axis,
values : Array[String],
) -> Bool raise @core.DataError {
if values.is_empty() && !self.units_state.explicit {
return false
}
if self.units_state.explicit &&
!(self.units_state.converter is Some(Category)) {
return true
}
let need_new = !self.units_state.explicit &&
!(self.units_state.converter is Some(Category))
self.units_state.converter = Some(Category)
match self.units_state.unit {
None =>
self.set_unit_tag(Some(AxisUnitTag::Category(CategoryUnits::new(values))))
Some(Category(units)) => ignore(units.encode(values))
Some(Date(_)) =>
raise @core.DataError::InvalidTicks(
message="'datetime.timezone' object has no attribute 'update'",
)
}
// default_units has already stored a non-None unit, so the original outer
// update_units also updates info on a newly selected converter.
if need_new {
self.update_unit_axisinfo()
}
true
}
///|
pub fn Axis::convert_category_units(
self : Axis,
values : Array[String],
) -> Array[Double] raise @core.DataError {
if self.units_state.converter is None {
if values.is_empty() {
return []
}
// convert_units discovers a converter but does not run default_units or
// axisinfo. Shared peers may already have units but no local converter.
self.units_state.converter = Some(Category)
}
match self.units_state.converter {
Some(Category) =>
match self.get_category_units() {
Some(units) => units.encode(values)
None =>
raise @core.DataError::InvalidTicks(
message="Missing category information for StrCategoryConverter",
)
}
_ =>
if values.is_empty() {
[]
} else {
raise @core.DataError::InvalidTicks(
message="unsupported typed date conversion of string category values: NumPy datetime64 string grammar is not represented",
)
}
}
}
///|
pub fn Axis::convert_date_units(
self : Axis,
values : Array[@date.RRuleDateTime],
) -> Array[Double] raise @core.DataError {
if values.is_empty() {
return []
}
if self.units_state.converter is None {
self.units_state.converter = Some(DefaultDate)
}
if self.units_state.converter is Some(Category) {
raise @core.DataError::InvalidTicks(
message="categorical converter cannot convert datetime values",
)
}
values.map(value => {
value.to_ordinal() catch {
@date.DateError::InvalidDate(message~)
| @date.DateError::InvalidFormat(message~) =>
raise @core.DataError::InvalidTicks(message~)
}
})
}
///|
fn Axis::replace_category_units_extension(
self : Axis,
units : CategoryUnits,
) -> Unit {
self.units_state.converter = Some(Category)
self.units_state.explicit = false
self.units_state.unit = Some(AxisUnitTag::Category(units))
}
///|
fn Axis::clear_category_units_extension(self : Axis) -> Unit {
if self.units_state.converter is Some(Category) ||
self.units_state.unit is Some(AxisUnitTag::Category(_)) {
self.units_state.converter = None
self.units_state.explicit = false
self.units_state.unit = None
}
}
///|
fn Axis::clear_date_units_extension(self : Axis) -> Unit {
if self.units_state.converter is Some(DefaultDate | ExplicitDate(_)) ||
self.units_state.unit is Some(AxisUnitTag::Date(_)) {
self.units_state.converter = None
self.units_state.explicit = false
self.units_state.unit = None
}
}
///|
/// Explicitly select the builtin categorical converter, without installing info.
pub fn Axis::set_explicit_category_converter(
self : Axis,
) -> Unit raise @core.DataError {
if self.units_state.explicit &&
!(self.units_state.converter is Some(Category)) {
raise @core.DataError::InvalidTicks(
message="Axis already has an explicit converter set",
)
}
self.units_state.converter = Some(Category)
self.units_state.explicit = true
}