///|
/// Locations supported for axis tick labels.
pub(all) enum TickPosition {
Top
Bottom
Left
Right
Both
}
///|
/// A coordinate axis with limits, tick formatting, and presentation settings.
pub struct Axis {
priv mut function_scale : AxisFunctionScale?
priv units_state : AxisUnitState
priv formatter_axis_binding : @ticker.FormatterAxisBinding
priv mut tickers : AxisNumericTickers
priv formatter_messages : Array[String]
priv mut minpos_provider : (() -> Double raise @core.DataError)?
priv mut shared_minpos_provider : (() -> Array[Double] raise @core.DataError)?
priv mut render_minpos_provider : (() -> Double raise @core.DataError)?
priv mut render_shared_minpos_provider : (() -> Array[Double] raise @core.DataError)?
priv mut autoscale_minpos_provider : (() -> Array[Double] raise @core.DataError)?
priv mut render_autoscale_minpos_provider : (() -> Array[Double] raise @core.DataError)?
priv mut data_interval_provider : (() -> (Double, Double))?
priv mut formatter_geometry_provider : (() -> (Double, Double))?
priv mut tick_layout_origin : Double?
priv mut tick_length_provider : (() -> Double)?
priv mut numeric_view_provider : (() -> (Double, Double) raise @core.DataError)?
priv mut render_view_provider : (() -> (Double, Double) raise @core.DataError)?
priv mut autoscale_view_provider : (() -> Unit raise @core.DataError)?
priv mut autoscale_enabled : Bool
priv mut autoscale_pending : Bool
priv mut stored_view : @core.Limits
priv mut tick_layout_length : Double?
priv mut tick_layout_view : (Double, Double)?
priv mut tick_layout_active : Bool
priv mut tick_horizontal : Bool
priv mut tick_label_size : Double?
mut limits : @core.Limits?
mut auto_view : @core.Limits?
mut inverted : Bool
mut label : String?
priv mut label_default : Bool
mut tick_count : Int32
priv mut tick_count_configured : Bool
priv mut date_converter : @date.DateConverter?
mut category : @category.CategoryAxis?
mut date_format : String?
mut date_offset_minutes : Int32
mut date_timezone : @date.DateTimeZone?
priv mut date_timezone_is_default : Bool
priv mut date_timezone_override : Bool
priv mut remove_minor_overlap : Bool
mut scale : @scale.Scale
mut tick_position : TickPosition?
mut grid_visible : Bool
mut grid_color : String
mut grid_width : Double
mut minor_grid_visible : Bool
mut minor_grid_color : String
mut minor_grid_width : Double
}
///|
/// Construct an axis with fresh linear locator and formatter defaults.
pub fn axis(horizontal~ : Bool = true) -> Axis {
let result : Axis = {
function_scale: None,
units_state: AxisUnitState::new(),
formatter_axis_binding: @ticker.FormatterAxisBinding::new(),
tickers: AxisNumericTickers::new(),
formatter_messages: [],
minpos_provider: None,
shared_minpos_provider: None,
render_minpos_provider: None,
render_shared_minpos_provider: None,
autoscale_minpos_provider: None,
render_autoscale_minpos_provider: None,
data_interval_provider: None,
formatter_geometry_provider: None,
tick_layout_origin: None,
tick_length_provider: None,
numeric_view_provider: None,
render_view_provider: None,
autoscale_view_provider: None,
autoscale_enabled: true,
autoscale_pending: false,
stored_view: @core.limits(0.0, 1.0),
tick_layout_length: None,
tick_layout_view: None,
tick_layout_active: false,
tick_horizontal: horizontal,
tick_label_size: None,
limits: None,
auto_view: None,
inverted: false,
label: None,
label_default: true,
tick_count: 6,
tick_count_configured: false,
date_converter: None,
category: None,
date_format: None,
date_offset_minutes: 0,
date_timezone: None,
date_timezone_is_default: true,
date_timezone_override: false,
remove_minor_overlap: true,
scale: @scale.linear_scale(),
tick_position: None,
grid_visible: @config.grid_visible(),
grid_color: @config.grid_color(),
grid_width: @config.grid_linewidth(),
minor_grid_visible: false,
minor_grid_color: @config.grid_color(),
minor_grid_width: @config.grid_linewidth() / 2.0,
}
result.formatter_axis_binding.bind_view_interval(() => {
let (lower, upper) = result.view_interval_checked()
@ticker.FormatterViewInterval::NdArray(
@ticker.TickValues::from_array([lower, upper]),
)
})
result.formatter_axis_binding.bind_transform_linthresh(() => {
result.scale
.symlog_linthresh()
.map(value => @ticker.ScalarNumber::Number(value))
})
// All linear constructors are pure. There is no date/category state or
// user callback here, so the checked setter's preconditions cannot fail.
try! result.set_scale(@scale.linear_scale())
result
}
///|
/// Set explicit limits while preserving their direction.
pub fn Axis::set_limits(
self : Axis,
lower : Double,
upper : Double,
) -> Unit raise @core.DataError {
let limits = @core.limits(lower, upper)
if !limits.is_finite() {
raise @core.DataError::NonFiniteLimit(axis="axis")
}
if self.function_scale is Some(_) {
ignore(self.set_bound_limits_checked(lower, upper, automatic=Some(false)))
return
}
if self.tickers.major_locator_kind != Numeric {
ignore(self.set_bound_limits_checked(lower, upper, automatic=Some(false)))
return
}
if self.scale.is_logit() {
let resolved = self.logit_limit_bounds_checked(lower, upper)
self.limits = Some(resolved)
self.auto_view = None
self.inverted = resolved.lower() > resolved.upper()
self.stored_view = resolved
self.autoscale_enabled = false
self.autoscale_pending = false
if self.tick_layout_view is Some(_) {
self.tick_layout_view = Some((resolved.lower(), resolved.upper()))
}
return
}
if self.scale.is_log() && (lower <= 0.0 || upper <= 0.0) {
raise @core.DataError::InvalidTicks(
message="log scale limits must be positive",
)
}
self.limits = Some(limits)
self.auto_view = None
self.inverted = lower > upper
self.stored_view = limits
self.autoscale_enabled = false
self.autoscale_pending = false
if self.tick_layout_view is Some(_) {
self.tick_layout_view = Some((lower, upper))
}
}
///|
/// Clear explicit limits and return to automatic range computation,
/// preserving the axis direction.
pub fn Axis::clear_limits(self : Axis) -> Unit {
self.limits = None
self.auto_view = None
self.autoscale_enabled = true
self.autoscale_pending = true
}
///|
/// Restore tickers, units and presentation while retaining the view and context.
/// Shared peers observe the replacement ticker objects through the same holder.
pub fn Axis::clear(self : Axis) -> Unit {
self.formatter_messages.clear()
self.tick_label_size = None
self.label = None
self.label_default = true
self.reset_unit_state()
self.tick_count = 6
self.tick_count_configured = false
self.tickers.automatic_date_locator = None
self.date_converter = None
self.category = None
self.date_format = None
self.date_offset_minutes = 0
self.date_timezone = None
self.date_timezone_is_default = true
self.date_timezone_override = false
self.tickers.date_locator = None
self.tickers.date_formatter = None
self.tickers.date_formatter_default = false
self.tickers.minor_date_locator = None
self.tickers.minor_date_formatter = None
self.tick_position = None
self.grid_visible = @config.grid_visible()
self.grid_color = @config.grid_color()
self.grid_width = @config.grid_linewidth()
self.minor_grid_visible = false
self.minor_grid_color = @config.grid_color()
self.minor_grid_width = @config.grid_linewidth() / 2.0
// Clear the typed converter guards before installing numeric defaults. The
// constructors/setters cannot fail in this linear, callback-free profile.
// Do not replace the holder or detach any previously retained object.
try! self.set_scale(@scale.linear_scale())
}
///|
/// Set the coordinate scale for this axis.
pub fn Axis::set_scale(
self : Axis,
value : @scale.Scale,
) -> Unit raise @core.DataError {
if value.is_nonlinear() && self.date_format is Some(_) {
raise @core.DataError::InvalidTicks(
message="date axis cannot be combined with a non-linear scale",
)
}
if value.is_log() && self.category is Some(_) {
raise @core.DataError::InvalidTicks(
message="log scale cannot be combined with category data",
)
}
self.scale = value
self.function_scale = None
self.install_scale_defaults()
self.mark_scale_defaults()
}
///|
/// Return the configured coordinate scale.
pub fn Axis::scale(self : Axis) -> @scale.Scale {
self.scale
}
///|
/// Return explicit limits, if any.
pub fn Axis::limits(self : Axis) -> @core.Limits? {
self.limits
}
///|
/// Set the axis label.
pub fn Axis::set_label(self : Axis, label : String) -> Unit {
self.label = Some(label)
self.label_default = false
}
///|
/// Return the optional axis label.
pub fn Axis::label(self : Axis) -> String? {
self.label
}
///|
/// Set the side on which this axis's tick labels are rendered.
///
/// The renderer interprets `Top`/`Bottom` for an x axis and
/// `Left`/`Right` for a y axis. `Both` renders labels on both sides.
pub fn Axis::set_tick_position(self : Axis, position : TickPosition) -> Unit {
self.tick_position = Some(position)
}
///|
/// Return the explicitly configured tick label side, if any.
pub fn Axis::tick_position(self : Axis) -> TickPosition? {
self.tick_position
}
///|
/// Restore the renderer's default tick label side.
pub fn Axis::clear_tick_position(self : Axis) -> Unit {
self.tick_position = None
}
///|
/// Configure major grid lines for this axis.
///
/// Grid lines are disabled by default, matching Matplotlib's
/// `axes.grid=False`. The renderer consumes the style without changing tick
/// positions or labels.
pub fn Axis::set_grid(
self : Axis,
visible : Bool,
color : String,
linewidth : Double,
) -> Unit raise @core.DataError {
if linewidth.is_nan() || linewidth.is_inf() || linewidth <= 0.0 {
raise @core.DataError::InvalidTicks(
message="grid linewidth must be finite and positive",
)
}
if color.length() == 0 {
raise @core.DataError::InvalidTicks(message="grid color must not be empty")
}
self.grid_visible = visible
self.grid_color = color
self.grid_width = linewidth
}
///|
/// Return whether major grid lines are enabled for this axis.
pub fn Axis::grid_visible(self : Axis) -> Bool {
self.grid_visible
}
///|
/// Return the major grid line color.
pub fn Axis::grid_color(self : Axis) -> String {
self.grid_color
}
///|
/// Return the major grid line width.
pub fn Axis::grid_width(self : Axis) -> Double {
self.grid_width
}
///|
/// Configure minor grid lines independently from major grids.
pub fn Axis::set_minor_grid(
self : Axis,
visible : Bool,
color : String,
linewidth : Double,
) -> Unit raise @core.DataError {
if linewidth.is_nan() || linewidth.is_inf() || linewidth <= 0.0 {
raise @core.DataError::InvalidTicks(
message="minor grid linewidth must be finite and positive",
)
}
if color.length() == 0 {
raise @core.DataError::InvalidTicks(
message="minor grid color must not be empty",
)
}
self.minor_grid_visible = visible
self.minor_grid_color = color
self.minor_grid_width = linewidth
}
///|
/// Return whether minor grid lines are enabled.
pub fn Axis::minor_grid_visible(self : Axis) -> Bool {
self.minor_grid_visible
}
///|
/// Return the minor grid line color.
pub fn Axis::minor_grid_color(self : Axis) -> String {
self.minor_grid_color
}
///|
/// Return the minor grid line width.
pub fn Axis::minor_grid_width(self : Axis) -> Double {
self.minor_grid_width
}
///|
/// Select the convenience scale sampler's desired tick count.
/// An explicitly installed locator keeps control of tick placement.
pub fn Axis::set_tick_count(self : Axis, count : Int32) -> Unit {
self.tick_count = if count < 2 { 2 } else { count }
self.tick_count_configured = true
if self.tickers.major_locator_default &&
self.tickers.major_locator_kind == Numeric {
// An explicit convenience count selects the existing scale sampler.
// It does not replace a locator that the user installed explicitly.
self.tickers.locator = None
self.tickers.major_locator_default = false
self.tickers.major_locator_kind = Numeric
}
self.tickers.automatic_date_locator = self.tickers.automatic_date_locator.map(value => {
try! value.with_maxticks(@date.All(self.tick_count))
},
)
}
///|
/// Return the desired number of ticks.
pub fn Axis::tick_count(self : Axis) -> Int32 {
self.tick_count
}
///|
/// Set a numeric locator for automatic major ticks.
pub fn Axis::set_locator(
self : Axis,
locator : @ticker.Locator,
) -> Unit raise @core.DataError {
if self.function_scale is None {
match (self.scale, locator) {
(
@scale.Scale::Log,
@ticker.Locator::Linear(..)
| @ticker.Locator::MaxN(..),
) =>
raise @core.DataError::InvalidTicks(
message="linear locator cannot be used with log scale",
)
(@scale.Scale::Log, @ticker.Locator::Fixed(values)) =>
for value in values {
if value <= 0.0 {
raise @core.DataError::InvalidTicks(
message="log scale locator values must be positive",
)
}
}
_ => ()
}
}
self.tickers.major_locator_default = false
self.tickers.major_locator_kind = Numeric
self.tickers.locator = Some(locator.copy())
self.bind_locator_data()
self.tickers.date_locator = None
self.tickers.fixed_ticks = None
self.tickers.fixed_labels = None
}
///|
/// Return the configured numeric locator, if any.
pub fn Axis::locator(self : Axis) -> @ticker.Locator? {
match self.tickers.locator {
Some(value) => Some(value.copy())
None => None
}
}
///|
/// Clear the numeric locator and use the convenience scale sampler.
pub fn Axis::clear_locator(self : Axis) -> Unit {
self.tickers.major_locator_default = false
self.tickers.major_locator_kind = Numeric
self.tickers.locator = None
self.tickers.fixed_ticks = None
self.tickers.fixed_labels = None
}
///|
/// Set a minor locator used between major tick positions.
pub fn Axis::set_minor_locator(
self : Axis,
locator : @ticker.MinorLocator,
) -> Unit raise @core.DataError {
if self.function_scale is None {
match (self.scale, locator) {
(@scale.Scale::Log, @ticker.MinorLocator::Auto(..)) =>
raise @core.DataError::InvalidTicks(
message="automatic minor locator cannot be used with log scale",
)
(@scale.Scale::Log, @ticker.MinorLocator::Fixed(values)) =>
for value in values {
if value <= 0.0 {
raise @core.DataError::InvalidTicks(
message="log scale minor locator values must be positive",
)
}
}
_ => ()
}
}
self.tickers.minor_locator_default = false
self.tickers.minor_locator_kind = Numeric
self.tickers.minor_locator = Some(locator.copy())
self.bind_locator_data(minor=true)
self.tickers.minor_date_locator = None
}
///|
/// Return the configured minor locator, if any.
pub fn Axis::minor_locator(self : Axis) -> @ticker.MinorLocator? {
match self.tickers.minor_locator {
Some(value) => Some(value.copy())
None => None
}
}
///|
/// Clear the minor locator.
pub fn Axis::clear_minor_locator(self : Axis) -> Unit {
self.tickers.minor_locator_default = false
self.tickers.minor_locator_kind = Numeric
self.tickers.minor_locator = None
self.tickers.minor_date_locator = None
}
///|
/// Set fixed tick positions, replacing automatic linear ticks.
pub fn Axis::set_ticks(
self : Axis,
values : Array[Double],
) -> Unit raise @core.DataError {
for value in values {
if value.is_nan() || value.is_inf() {
raise @core.DataError::InvalidTicks(
message="tick positions must be finite",
)
}
if self.scale.is_log() && value <= 0.0 {
raise @core.DataError::InvalidTicks(
message="log scale tick positions must be positive",
)
}
}
self.tickers.major_locator_default = false
self.tickers.major_locator_kind = Numeric
self.tickers.fixed_ticks = Some(values.copy())
self.tickers.fixed_labels = None
self.tickers.locator = None
self.tickers.date_locator = None
}
///|
/// Configure this axis to format numeric day values as dates.
///
/// An empty format selects a range-dependent format at render time. The
/// numeric values remain Matplotlib day ordinals, so existing plot methods
/// can be used after converting dates with `DateTime::to_ordinal`.
pub fn Axis::set_date_format(
self : Axis,
format : String,
) -> Unit raise @core.DataError {
self.configure_date_format(format)
}
///|
fn Axis::configure_date_format(
self : Axis,
format : String,
unit? : @date.DateTimeZone,
) -> Unit raise @core.DataError {
if self.scale.is_nonlinear() {
raise @core.DataError::InvalidTicks(
message="date axis cannot be combined with a non-linear scale",
)
}
if self.get_category_units() is Some(_) {
raise @core.DataError::InvalidTicks(
message="date axis cannot be combined with category data",
)
}
match self.category {
Some(_) =>
raise @core.DataError::InvalidTicks(
message="date axis cannot be combined with category data",
)
None => ()
}
let converter = self.date_converter.unwrap_or(@date.default_date_converter())
let unit = match unit {
Some(_) => unit
None if !self.date_timezone_is_default =>
Some(self.explicit_date_timezone())
None => None
}
let info = self.checked_date_axisinfo(converter, unit?)
self.date_format = Some(format)
self.date_converter = Some(converter)
if self.date_timezone_is_default &&
(self.date_timezone is None || format != "") {
self.date_timezone = info.major_locator().timezone()
self.date_offset_minutes = info.major_locator().offset_minutes()
}
self.install_date_unit_defaults(info)
if format != "" && self.tickers.fixed_labels is None {
self.tickers.major_formatter_kind = Date
self.tickers.major_formatter_default = false
self.tickers.date_format = Some(format)
self.tickers.date_formatter = None
self.tickers.date_formatter_default = false
self.tickers.formatter = None
self.tickers.named_formatter = None
self.tickers.fixed_labels = None
}
self.sync_default_date_formatter()
self.capture_date_units_extension()
self.formatter_messages.clear()
}
///|
/// Configure automatic date formatting for this axis.
pub fn Axis::set_date(self : Axis) -> Unit raise @core.DataError {
self.set_date_format("")
}
///|
/// Set the fixed UTC offset for date tick positions and labels.
///
/// ```mbt check
/// test {
/// let axis = @axis.axis()
/// try! axis.set_date_offset(345)
/// assert_eq(axis.date_offset_minutes(), 345)
/// }
/// ```
pub fn Axis::set_date_offset(
self : Axis,
offset_minutes : Int32,
) -> Unit raise @core.DataError {
if offset_minutes <= -1440 || offset_minutes >= 1440 {
raise @core.DataError::InvalidTicks(
message="date offset must be in -1439..1439 minutes",
)
}
if self.date_format is None {
self.configure_date_format(
"",
unit=try! @date.fixed_timezone(offset_minutes),
)
}
self.date_timezone = None
self.date_timezone_is_default = false
self.date_timezone_override = true
match self.tickers.date_locator {
Some(locator) =>
self.tickers.date_locator = Some(try! locator.with_offset(offset_minutes))
None => ()
}
self.date_offset_minutes = offset_minutes
self.tickers.automatic_date_locator = self.tickers.automatic_date_locator.map(value => {
try! value.with_offset(offset_minutes)
},
)
self.tickers.date_formatter = self.tickers.date_formatter.map(formatter => {
try! formatter.with_offset(offset_minutes)
})
self.tickers.minor_date_locator = self.tickers.minor_date_locator.map(locator => {
try! locator.with_offset(offset_minutes)
})
self.tickers.minor_date_formatter = self.tickers.minor_date_formatter.map(formatter => {
try! formatter.with_offset(offset_minutes)
})
self.capture_date_units_extension()
}
///|
/// Return the fixed UTC offset used for date tick positions and labels.
pub fn Axis::date_offset_minutes(self : Axis) -> Int32 {
self.date_offset_minutes
}
///|
/// Set an explicit transition-aware timezone for date labels.
///
/// Calendar tick positions and label conversion follow the configured UTC
/// transitions. Spring gaps are skipped and fall folds can emit both instants.
pub fn Axis::set_date_timezone(
self : Axis,
timezone : @date.DateTimeZone,
) -> Unit raise @core.DataError {
if self.date_format is None {
self.configure_date_format("", unit=timezone)
}
let offset_minutes = timezone.initial_offset_minutes()
match self.tickers.date_locator {
Some(locator) =>
self.tickers.date_locator = Some(locator.with_timezone(timezone))
None => ()
}
self.date_offset_minutes = offset_minutes
self.date_timezone = Some(timezone)
self.date_timezone_is_default = false
self.date_timezone_override = true
self.tickers.automatic_date_locator = self.tickers.automatic_date_locator.map(value => {
value.with_timezone(timezone)
},
)
self.tickers.date_formatter = self.tickers.date_formatter.map(formatter => {
formatter.with_timezone(timezone)
})
self.tickers.minor_date_locator = self.tickers.minor_date_locator.map(locator => {
locator.with_timezone(timezone)
})
self.tickers.minor_date_formatter = self.tickers.minor_date_formatter.map(formatter => {
formatter.with_timezone(timezone)
})
self.capture_date_units_extension()
}
///|
/// Return the transition-aware timezone used for date labels, if configured.
pub fn Axis::date_timezone(self : Axis) -> @date.DateTimeZone? {
self.date_timezone
}
///|
/// Use an explicit calendar locator while retaining automatic date labels.
pub fn Axis::set_date_locator(
self : Axis,
locator : @date.DateLocator,
) -> Unit raise @core.DataError {
if self.scale.is_nonlinear() {
raise @core.DataError::InvalidTicks(
message="date axis cannot be combined with a non-linear scale",
)
}
match self.date_format {
Some(_) => ()
None => self.set_date()
}
let effective_locator = if !self.date_timezone_override {
locator.copy()
} else {
match self.date_timezone {
Some(timezone) => locator.with_timezone(timezone)
None => locator.copy()
}
}
self.tickers.date_locator = Some(effective_locator)
self.date_offset_minutes = effective_locator.offset_minutes()
if self.date_timezone_is_default && effective_locator.timezone() is None {
// Preserve the legacy explicit fixed-offset locator extension. Ordinary
// constructors now carry a captured zone, so their labels remain dynamic.
self.date_timezone = None
self.date_timezone_is_default = false
self.date_timezone_override = true
}
self.sync_default_date_formatter()
self.tickers.major_locator_kind = Date
self.tickers.major_locator_default = false
self.tickers.locator = None
self.tickers.fixed_ticks = None
self.bind_locator_data()
self.formatter_messages.clear()
self.capture_date_units_extension()
}
///|
/// Clear date formatting and return to numeric tick labels.
pub fn Axis::clear_date(self : Axis) -> Unit {
self.clear_date_units_extension()
if self.tickers.major_locator_kind == Date {
self.tickers.major_locator_kind = Numeric
}
if self.tickers.minor_locator_kind == Date {
self.tickers.minor_locator_kind = Numeric
}
if self.tickers.major_formatter_kind == Date {
self.tickers.major_formatter_kind = Numeric
}
if self.tickers.minor_formatter_kind == Date {
self.tickers.minor_formatter_kind = Numeric
}
self.date_format = None
self.tickers.date_formatter = None
self.tickers.automatic_date_locator = None
self.date_converter = None
self.tickers.date_formatter_default = false
self.tickers.minor_date_locator = None
self.tickers.minor_date_formatter = None
self.date_timezone = None
self.date_timezone_is_default = true
self.date_timezone_override = false
self.tickers.date_locator = None
self.date_offset_minutes = 0
}
///|
/// Return the explicit date format, if date mode is enabled.
pub fn Axis::date_format(self : Axis) -> String? {
self.date_format
}
///|
/// Return the explicit date locator, if configured.
pub fn Axis::date_locator(self : Axis) -> @date.DateLocator? {
self.tickers.date_locator.map(value => value.copy())
}
///|
/// Configure the numeric formatter used for this axis's tick labels.
pub fn Axis::set_formatter(self : Axis, formatter : @ticker.Formatter) -> Unit {
self.tickers.major_formatter_default = false
self.tickers.major_formatter_kind = Numeric
self.tickers.formatter = Some(formatter.copy())
self.tickers.named_formatter = None
self.formatter_messages.clear()
}
///|
/// Return the configured numeric formatter, if any.
pub fn Axis::formatter(self : Axis) -> @ticker.Formatter? {
match self.tickers.formatter {
Some(value) => Some(value.copy())
None => None
}
}
///|
/// Return the shared text selected by the numeric or date formatter, if any.
pub fn Axis::formatter_offset_label(
self : Axis,
values : Array[Double],
) -> String {
self.formatter_offset_label_with_view(values, self.limits)
}
///|
/// Return the numeric or date formatter's offset label for an explicit viewport.
///
/// Renderers should pass the viewport they used to generate the ticks. When
/// it is omitted, an explicitly configured axis limit is used and otherwise
/// the formatter falls back to the supplied tick extent.
pub fn Axis::formatter_offset_label_with_view(
self : Axis,
values : Array[Double],
view : @core.Limits?,
) -> String {
if self.tickers.major_formatter_kind == Numeric &&
self.tickers.named_formatter is Some(value) {
return value.get_offset_checked() catch { _ => "" }
}
// Explicit labels own their text, and category axes have no shared offset.
if self.tickers.fixed_labels is Some(_) ||
self.category_for_labels() is Some(_) {
return ""
}
if self.date_format_for_labels() is Some(_) {
return match self.tickers.date_formatter {
Some(formatter) => {
let (ticks, inverted, _) = self.date_formatter_values(values, view)
formatter
.with_locator_unit(self.date_formatter_unit(values, view, false))
.offset_label(ticks, inverted~)
}
None => ""
}
}
let effective_view = match view {
Some(_) => view
None => self.limits
}
match self.tickers.formatter {
Some(value) => value.offset_label_with_view(values, effective_view)
None => ""
}
}
///|
/// Clear the numeric formatter and restore scale-based labels.
pub fn Axis::clear_formatter(self : Axis) -> Unit {
self.tickers.major_formatter_default = false
self.tickers.major_formatter_kind = Numeric
self.tickers.formatter = None
self.tickers.named_formatter = None
}
///|
/// Clear fixed tick positions and labels.
pub fn Axis::clear_ticks(self : Axis) -> Unit {
self.tickers.fixed_ticks = None
self.tickers.fixed_labels = None
}
///|
/// Configure an axis with an ordered set of string categories.
pub fn Axis::set_categories(
self : Axis,
labels : Array[String],
) -> Unit raise @core.DataError {
self.set_category_units(CategoryUnits::new(labels))
}
///|
/// Replace the categorical convenience configuration with a shared live mapping.
/// This explicit replacement extension does not emit a source units event.
pub fn Axis::set_category_units(
self : Axis,
units : CategoryUnits,
) -> Unit raise @core.DataError {
if self.scale.is_nonlinear() {
raise @core.DataError::InvalidTicks(
message="category data cannot be combined with a non-linear scale",
)
}
match self.date_format {
Some(_) =>
raise @core.DataError::InvalidTicks(
message="category data cannot be combined with date axis",
)
None => ()
}
let converter = @category.category().with_labels(units.labels())
self.category = Some(converter)
self.replace_category_units_extension(units)
self.tickers.major_locator_default = true
self.tickers.major_formatter_default = true
self.install_category_unit_defaults(units)
self.formatter_messages.clear()
}
///|
/// Clear category conversion and return to numeric tick generation.
pub fn Axis::clear_categories(self : Axis) -> Unit {
self.clear_category_units_extension()
self.category = None
if self.tickers.major_locator_kind == Category {
self.tickers.major_locator_kind = Numeric
}
if self.tickers.major_formatter_kind == Category {
self.tickers.major_formatter_kind = Numeric
}
}
///|
/// Return category labels in first-seen order, if configured.
pub fn Axis::categories(self : Axis) -> Array[String]? {
match self.get_category_units() {
Some(units) => Some(units.labels())
None => self.category.map(converter => converter.labels())
}
}
///|
/// Convert string values and retain their categories on this axis.
pub fn Axis::category_positions(
self : Axis,
values : Array[String],
) -> Array[Double] raise @core.DataError {
if !self.have_units() {
ignore(self.update_category_units(values))
}
self.convert_category_units(values)
}
///|
/// Set labels for the currently fixed tick positions.
pub fn Axis::set_tick_labels(
self : Axis,
labels : Array[String],
) -> Unit raise @core.DataError {
match self.tickers.fixed_ticks {
Some(values) =>
if values.length() != labels.length() {
raise @core.DataError::InvalidTicks(
message="tick labels must match tick positions",
)
} else {
self.tickers.major_formatter_default = false
self.tickers.major_formatter_kind = Numeric
self.tickers.fixed_labels = Some(labels.copy())
self.tickers.named_formatter = None
}
None =>
raise @core.DataError::InvalidTicks(
message="set tick positions before tick labels",
)
}
}
///|
/// Return fixed tick positions, if configured.
pub fn Axis::ticks(self : Axis) -> Array[Double]? {
match self.tickers.fixed_ticks {
Some(values) => Some(values.copy())
None => None
}
}
///|
/// Format labels for a concrete tick list.
pub fn Axis::tick_labels(self : Axis, values : Array[Double]) -> Array[String] {
self.tick_labels_with_view(values, self.limits)
}
///|
/// Format labels for a concrete tick list and viewport.
pub fn Axis::tick_labels_with_view(
self : Axis,
values : Array[Double],
view : @core.Limits?,
) -> Array[String] {
if self.tickers.major_formatter_kind == Numeric &&
self.tickers.named_formatter is Some(value) {
return value.format_ticks(values) catch { _ => [] }
}
match self.tickers.fixed_labels {
Some(labels) => labels.copy()
None =>
match self.category_for_labels() {
Some(converter) => converter.format_values(values) catch { _ => [] }
None => {
let result = Array(capacity=values.length())
match self.date_format_for_labels() {
Some(format) => {
if self.tickers.date_formatter is Some(formatter) {
let (ticks, inverted, reversed) = self.date_formatter_values(
values, view,
)
let labels = formatter
.with_locator_unit(
self.date_formatter_unit(values, view, false),
)
.format_values(ticks, inverted~)
return if reversed { labels.rev() } else { labels }
}
let timezone = self.date_timezone
let labels = match timezone {
Some(timezone) =>
@date.format_date_ticks(
values,
format~,
offset_minutes=self.date_offset_minutes,
timezone~,
)
None =>
@date.format_date_ticks(
values,
format~,
offset_minutes=self.date_offset_minutes,
)
}
for label in labels {
result.push(label)
}
}
None =>
match self.tickers.formatter {
Some(formatter) => {
let effective_view = match view {
Some(_) => view
None => self.limits
}
let labels = formatter.format_values_with_view(
values, effective_view,
)
for label in labels {
result.push(label)
}
}
None =>
for value in values {
result.push(self.scale.format_tick(value))
}
}
}
result
}
}
}
}
///|
/// Generate ticks for explicit or supplied automatic limits.
pub fn Axis::tick_values(
self : Axis,
automatic : @core.Limits,
) -> Array[Double] {
if self.tickers.fixed_ticks is None &&
(self.category_for_ticks() is None || self.numeric_tickers_active()) &&
self.tickers.locator
is Some(
@ticker.Locator::NamedLogit(_)
| @ticker.Locator::NamedAsinh(_)
| @ticker.Locator::NamedSymLog(_)
| @ticker.Locator::NamedLog(_)
| @ticker.Locator::Null(_)
| @ticker.Locator::NamedLinear(_)
| @ticker.Locator::NamedFixed(_)
| @ticker.Locator::Index(_)
| @ticker.Locator::NamedMaxN(_)
| @ticker.Locator::NamedAuto(_)
) {
return self.tick_values_checked(automatic) catch { _ => [] }
}
match self.tickers.fixed_ticks {
Some(values) => values.copy()
None =>
match self.category_for_ticks() {
Some(converter) => converter.positions()
None => {
let limits = match self.limits {
Some(value) => value
None => automatic
}
let limits = self.date_nonsingular_bounds(
limits.lower(),
limits.upper(),
)
match self.date_format_for_ticks() {
Some(_) =>
match self.tickers.date_locator {
Some(locator) =>
locator.tick_values(limits.lower(), limits.upper())
None => {
if self.tickers.locator is Some(locator) {
return locator.tick_values(limits.lower(), limits.upper())
}
if self.tickers.automatic_date_locator is Some(locator) {
return locator.tick_values(limits.lower(), limits.upper()) catch {
_ => []
}
}
let timezone = self.date_timezone
@date.date_tick_values(
limits.lower(),
limits.upper(),
count=self.tick_count,
offset_minutes=self.date_offset_minutes,
timezone?,
)
}
}
None =>
match self.tickers.locator {
Some(locator) =>
locator.tick_values(limits.lower(), limits.upper())
None => self.scale.tick_values(limits, count=self.tick_count)
}
}
}
}
}
}
///|
/// Generate minor ticks from the configured minor locator.
pub fn Axis::minor_tick_values(
self : Axis,
automatic : @core.Limits,
) -> Array[Double] {
if self.tickers.minor_locator_kind == Numeric &&
self.tickers.minor_formatter_kind != Numeric &&
self.tickers.minor_locator is Some(@ticker.MinorLocator::Fixed(_)) {
return self.minor_tick_values_checked(automatic) catch { _ => [] }
}
if self.tickers.minor_locator_kind == Numeric &&
self.tickers.minor_locator
is Some(
@ticker.MinorLocator::NamedLogit(_)
| @ticker.MinorLocator::NamedAsinh(_)
| @ticker.MinorLocator::NamedSymLog(_)
| @ticker.MinorLocator::NamedLog(_)
| @ticker.MinorLocator::Null(_)
| @ticker.MinorLocator::NamedAutoMinor(_)
| @ticker.MinorLocator::NamedLinear(_)
| @ticker.MinorLocator::Index(_)
| @ticker.MinorLocator::NamedFixed(_)
| @ticker.MinorLocator::NamedMaxN(_)
| @ticker.MinorLocator::NamedAuto(_)
) {
return self.minor_tick_values_checked(automatic) catch { _ => [] }
}
if self.tickers.minor_locator_kind == Date &&
self.tickers.minor_date_locator is Some(locator) {
let limits = self.limits.unwrap_or(automatic)
let limits = self.date_nonsingular_bounds(limits.lower(), limits.upper())
let ticks = locator.tick_values(limits.lower(), limits.upper())
return self.filter_date_minor_ticks(
ticks,
self.tick_values(automatic),
limits,
)
}
match self.tickers.minor_locator {
None => []
Some(locator) => {
let limits = match self.limits {
Some(value) => value
None => automatic
}
let major = self.tick_values(automatic)
if self.date_format_for_ticks() is Some(_) {
let ticks = match locator {
@ticker.MinorLocator::Fixed(_) =>
locator.tick_values([], limits.lower(), limits.upper())
_ => locator.tick_values(major, limits.lower(), limits.upper())
}
self.filter_date_minor_ticks(ticks, major, limits)
} else {
locator.tick_values(major, limits.lower(), limits.upper())
}
}
}
}