Skip to content

Commit

Permalink
Unify the time formatting between the time panel and the plot (#1369)
Browse files Browse the repository at this point in the history
* Unify the time formatting between the time panel and the plot

* typo

* Add a link explaining how leap-seconds work in UTC
  • Loading branch information
emilk authored Feb 22, 2023
1 parent 49cc353 commit 0bc2e89
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 103 deletions.
26 changes: 26 additions & 0 deletions crates/re_log_types/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ impl Time {
20 <= years_since_epoch && years_since_epoch <= 150
}

/// Returns the absolute datetime, if this is a valid, unambiguous, absolute time.
pub fn to_chrono(&self) -> Option<chrono::DateTime<chrono::Utc>> {
let ns_since_epoch = self.nanos_since_epoch();
if self.is_abolute_date() {
use chrono::TimeZone as _;
if let chrono::LocalResult::Single(datetime) = chrono::Utc.timestamp_opt(
ns_since_epoch / 1_000_000_000,
(ns_since_epoch % 1_000_000_000) as _,
) {
Some(datetime)
} else {
None
}
} else {
None
}
}

pub fn is_exactly_midnight(&self) -> bool {
// This is correct despite leap seconds because
// during positive leap seconds, UTC actually has a discontinuity
// (the same integer is reused for two different times).
// See https://en.wikipedia.org/wiki/Unix_time#Leap_seconds
self.nanos_since_epoch() % (24 * 60 * 60 * 1_000_000_000) == 0
}

/// Human-readable formatting
pub fn format(&self) -> String {
let nanos_since_epoch = self.nanos_since_epoch();
Expand Down
72 changes: 72 additions & 0 deletions crates/re_viewer/src/misc/format_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/// Useful when showing dates/times on a timeline
/// and you want it compact.
///
/// Shows dates when zoomed out, shows times when zoomed in,
/// shows relative millisecond when really zoomed in.
pub fn format_time_compact(time: re_log_types::Time) -> String {
let ns = time.nanos_since_epoch();
let relative_ns = ns % 1_000_000_000;
let is_whole_second = relative_ns == 0;
if is_whole_second {
if let Some(datetime) = time.to_chrono() {
return if time.is_exactly_midnight() {
// Show just the date:
datetime.format("%Y-%m-%dZ").to_string()
} else {
// Show just the time:
let is_whole_minute = ns % 60_000_000_000 == 0;
let is_whole_second = ns % 1_000_000_000 == 0;
if is_whole_minute {
datetime.time().format("%H:%MZ").to_string()
} else if is_whole_second {
datetime.time().format("%H:%M:%SZ").to_string()
} else {
datetime.time().format("%H:%M:%S%.3fZ").to_string()
}
};
}

re_log_types::Duration::from_nanos(ns).to_string()
} else {
// We are in the sub-second resolution.
// Showing the full time (HH:MM:SS.XXX or 3h 2m 6s …) becomes too long,
// so instead we switch to showing the time as milliseconds since the last whole second:
let ms = relative_ns as f64 * 1e-6;
if relative_ns % 1_000_000 == 0 {
format!("{ms:+.0} ms")
} else if relative_ns % 100_000 == 0 {
format!("{ms:+.1} ms")
} else if relative_ns % 10_000 == 0 {
format!("{ms:+.2} ms")
} else if relative_ns % 1_000 == 0 {
format!("{ms:+.3} ms")
} else if relative_ns % 100 == 0 {
format!("{ms:+.4} ms")
} else if relative_ns % 10 == 0 {
format!("{ms:+.5} ms")
} else {
format!("{ms:+.6} ms")
}
}
}

/// When showing grid-lines representing time.
///
/// Given some spacing (e.g. 10s), return the next spacing (60s).
pub fn next_grid_tick_magnitude_ns(spacing_ns: i64) -> i64 {
if spacing_ns <= 1_000_000_000 {
spacing_ns * 10 // up to 10 second ticks
} else if spacing_ns == 10_000_000_000 {
spacing_ns * 6 // to the whole minute
} else if spacing_ns == 60_000_000_000 {
spacing_ns * 10 // to ten minutes
} else if spacing_ns == 600_000_000_000 {
spacing_ns * 6 // to an hour
} else if spacing_ns == 60 * 60 * 1_000_000_000 {
spacing_ns * 12 // to 12 h
} else if spacing_ns == 12 * 60 * 60 * 1_000_000_000 {
spacing_ns * 2 // to a day
} else {
spacing_ns.checked_mul(10).unwrap_or(spacing_ns) // multiple of ten days
}
}
1 change: 1 addition & 0 deletions crates/re_viewer/src/misc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod app_options;
pub mod caches;
pub mod color_map;
pub mod format_time;
mod item;
pub(crate) mod mesh_loader;
mod selection_state;
Expand Down
54 changes: 3 additions & 51 deletions crates/re_viewer/src/ui/time_panel/paint_ticks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use egui::{lerp, pos2, remap_clamp, Align2, Color32, Rect, Rgba, Shape, Stroke};

use re_log_types::{Time, TimeRangeF, TimeReal, TimeType};

use crate::misc::format_time::next_grid_tick_magnitude_ns;

use super::time_ranges_ui::TimeRangesUi;

pub fn paint_time_ranges_and_ticks(
Expand Down Expand Up @@ -59,56 +61,6 @@ fn paint_time_range_ticks(

match time_type {
TimeType::Time => {
fn next_grid_tick_magnitude_ns(spacing_ns: i64) -> i64 {
if spacing_ns <= 1_000_000_000 {
spacing_ns * 10 // up to 10 second ticks
} else if spacing_ns == 10_000_000_000 {
spacing_ns * 6 // to the whole minute
} else if spacing_ns == 60_000_000_000 {
spacing_ns * 10 // to ten minutes
} else if spacing_ns == 600_000_000_000 {
spacing_ns * 6 // to an hour
} else if spacing_ns == 60 * 60 * 1_000_000_000 {
spacing_ns * 12 // to 12 h
} else if spacing_ns == 12 * 60 * 60 * 1_000_000_000 {
spacing_ns * 2 // to a day
} else {
spacing_ns.checked_mul(10).unwrap_or(spacing_ns) // multiple of ten days
}
}

fn grid_text_from_ns(ns: i64) -> String {
let relative_ns = ns % 1_000_000_000;
if relative_ns == 0 {
let time = Time::from_ns_since_epoch(ns);
if time.is_abolute_date() {
time.format_time("%H:%M:%S")
} else {
re_log_types::Duration::from_nanos(ns).to_string()
}
} else {
// We are in the sub-second resolution.
// Showing the full time (HH:MM:SS.XXX or 3h 2m 6s …) becomes too long,
// so instead we switch to showing the time as milliseconds since the last whole second:
let ms = relative_ns as f64 * 1e-6;
if relative_ns % 1_000_000 == 0 {
format!("{ms:+.0} ms")
} else if relative_ns % 100_000 == 0 {
format!("{ms:+.1} ms")
} else if relative_ns % 10_000 == 0 {
format!("{ms:+.2} ms")
} else if relative_ns % 1_000 == 0 {
format!("{ms:+.3} ms")
} else if relative_ns % 100 == 0 {
format!("{ms:+.4} ms")
} else if relative_ns % 10 == 0 {
format!("{ms:+.5} ms")
} else {
format!("{ms:+.6} ms")
}
}
}

paint_ticks(
ui.ctx(),
ui.visuals().dark_mode,
Expand All @@ -117,7 +69,7 @@ fn paint_time_range_ticks(
&ui.clip_rect(),
time_range, // ns
next_grid_tick_magnitude_ns,
grid_text_from_ns,
|ns| crate::misc::format_time::format_time_compact(Time::from_ns_since_epoch(ns)),
)
}
TimeType::Sequence => {
Expand Down
64 changes: 12 additions & 52 deletions crates/re_viewer/src/ui/view_time_series/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use egui::{

use re_arrow_store::TimeType;

use crate::{ui::view_time_series::scene::PlotSeriesKind, ViewerContext};
use crate::{
misc::format_time::next_grid_tick_magnitude_ns, ui::view_time_series::scene::PlotSeriesKind,
ViewerContext,
};

use super::SceneTimeSeries;

Expand Down Expand Up @@ -140,72 +143,29 @@ pub(crate) fn view_time_series(

fn format_time(time_type: TimeType, time_int: i64) -> String {
if time_type == TimeType::Time {
let ns_since_epoch = time_int;
let time = re_log_types::Time::from_ns_since_epoch(ns_since_epoch);
if time.is_abolute_date() {
use chrono::TimeZone as _;
if let chrono::LocalResult::Single(datetime) = chrono::Utc.timestamp_opt(
ns_since_epoch / 1_000_000_000,
(ns_since_epoch % 1_000_000_000) as _,
) {
let is_start_of_new_day =
datetime.time() == chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap();
if is_start_of_new_day {
// Show just the date:
return datetime.format("%Y-%m-%dZ").to_string();
} else {
// Show just the time:
let is_whole_minute = ns_since_epoch % 60_000_000_000 == 0;
let is_whole_second = ns_since_epoch % 1_000_000_000 == 0;
if is_whole_minute {
return datetime.time().format("%H:%MZ").to_string();
} else if is_whole_second {
return datetime.time().format("%H:%M:%SZ").to_string();
} else {
return datetime.time().format("%H:%M:%S%.3fZ").to_string();
}
}
}
}
let time = re_log_types::Time::from_ns_since_epoch(time_int);
crate::misc::format_time::format_time_compact(time)
} else {
time_type.format(re_log_types::TimeInt::from(time_int))
}

time_type.format(re_log_types::TimeInt::from(time_int))
}

fn ns_grid_spacer(
canvas_size: egui::Vec2,
input: &egui::plot::GridInput,
) -> Vec<egui::plot::GridMark> {
fn next_time_step(spacing_ns: i64) -> i64 {
if spacing_ns <= 1_000_000_000 {
spacing_ns * 10 // up to 10 second ticks
} else if spacing_ns == 10_000_000_000 {
spacing_ns * 6 // to the whole minute
} else if spacing_ns == 60_000_000_000 {
spacing_ns * 10 // to ten minutes
} else if spacing_ns == 600_000_000_000 {
spacing_ns * 6 // to an hour
} else if spacing_ns == 60 * 60 * 1_000_000_000 {
spacing_ns * 12 // to 12 h
} else if spacing_ns == 12 * 60 * 60 * 1_000_000_000 {
spacing_ns * 2 // to a day
} else {
spacing_ns.checked_mul(10).unwrap_or(spacing_ns) // multiple of ten days
}
}

let minimum_medium_line_spacing = 150.0; // ≈min size of a label
let max_medium_lines = canvas_size.x as f64 / minimum_medium_line_spacing;

let (min_ns, max_ns) = input.bounds;
let width_ns = max_ns - min_ns;

let mut small_spacing_ns = 1;
while width_ns / (next_time_step(small_spacing_ns) as f64) > max_medium_lines {
small_spacing_ns = next_time_step(small_spacing_ns);
while width_ns / (next_grid_tick_magnitude_ns(small_spacing_ns) as f64) > max_medium_lines {
small_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns);
}
let medium_spacing_ns = next_time_step(small_spacing_ns);
let big_spacing_ns = next_time_step(medium_spacing_ns);
let medium_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns);
let big_spacing_ns = next_grid_tick_magnitude_ns(medium_spacing_ns);

let mut current_ns = (min_ns.floor() as i64) / small_spacing_ns * small_spacing_ns;
let mut marks = vec![];
Expand Down

1 comment on commit 0bc2e89

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust Benchmark

Benchmark suite Current: 0bc2e89 Previous: 49cc353 Ratio
datastore/insert/batch/rects/insert 560773 ns/iter (± 1724) 556108 ns/iter (± 3956) 1.01
datastore/latest_at/batch/rects/query 1805 ns/iter (± 28) 1802 ns/iter (± 2) 1.00
datastore/latest_at/missing_components/primary 357 ns/iter (± 0) 356 ns/iter (± 0) 1.00
datastore/latest_at/missing_components/secondaries 425 ns/iter (± 0) 426 ns/iter (± 0) 1.00
datastore/range/batch/rects/query 150127 ns/iter (± 234) 156362 ns/iter (± 326) 0.96
mono_points_arrow/generate_message_bundles 53231303 ns/iter (± 609706) 48469512 ns/iter (± 403170) 1.10
mono_points_arrow/generate_messages 139356081 ns/iter (± 1459025) 126972280 ns/iter (± 1106990) 1.10
mono_points_arrow/encode_log_msg 173224585 ns/iter (± 2468141) 161075015 ns/iter (± 1281351) 1.08
mono_points_arrow/encode_total 369964000 ns/iter (± 3903155) 331800152 ns/iter (± 1693491) 1.12
mono_points_arrow/decode_log_msg 190135352 ns/iter (± 1428624) 178175209 ns/iter (± 726050) 1.07
mono_points_arrow/decode_message_bundles 77249933 ns/iter (± 1168466) 66201769 ns/iter (± 753078) 1.17
mono_points_arrow/decode_total 269983640 ns/iter (± 2093104) 242208082 ns/iter (± 1432365) 1.11
batch_points_arrow/generate_message_bundles 324793 ns/iter (± 879) 336737 ns/iter (± 481) 0.96
batch_points_arrow/generate_messages 6404 ns/iter (± 14) 6393 ns/iter (± 49) 1.00
batch_points_arrow/encode_log_msg 373777 ns/iter (± 3057) 361711 ns/iter (± 1443) 1.03
batch_points_arrow/encode_total 726038 ns/iter (± 2838) 726083 ns/iter (± 2457) 1.00
batch_points_arrow/decode_log_msg 349642 ns/iter (± 1427) 353115 ns/iter (± 2090) 0.99
batch_points_arrow/decode_message_bundles 2128 ns/iter (± 5) 2117 ns/iter (± 7) 1.01
batch_points_arrow/decode_total 361712 ns/iter (± 2120) 360059 ns/iter (± 2407) 1.00
arrow_mono_points/insert 7471020932 ns/iter (± 177013735) 6297405427 ns/iter (± 30288246) 1.19
arrow_mono_points/query 1780967 ns/iter (± 14889) 1781744 ns/iter (± 19876) 1.00
arrow_batch_points/insert 2713162 ns/iter (± 31137) 2675624 ns/iter (± 29377) 1.01
arrow_batch_points/query 16846 ns/iter (± 36) 16835 ns/iter (± 38) 1.00
tuid/Tuid::random 34 ns/iter (± 0) 34 ns/iter (± 0) 1

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.