Skip to content

Commit

Permalink
feat: add unit selector with seconds and bytes (#13)
Browse files Browse the repository at this point in the history
uses `humantime` and `ubyte`

Closes #12
  • Loading branch information
DDtKey authored Jan 27, 2024
1 parent 7a4771a commit 3dfab4d
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 85 deletions.
49 changes: 43 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions crates/web-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chart-js-rs = { version = "0.0.13" , git = "https://github.com/DDtKey/chart-js-rs.git", branch = "support-chart-updates"}
console_error_panic_hook = { version = "0.1.7", optional = true }
console_log = { version = "1", optional = true }
log = { version = "0.4"}
humantime = { version = "2.1.0" }
leptos = { version = "0.5.7", features = ["csr"] }
leptos_router = { version = "0.5.7", features = ["csr"] }
leptos-use = { version = "0.9.0", git = "https://github.com/synphonyte/leptos-use", rev = "67fa44a5acc8a228350820fd612e2d25a7fb64a8" }
chart-js-rs = { version = "0.0.13" , git = "https://github.com/DDtKey/chart-js-rs.git", branch = "support-chart-updates"}

leptos_router = { version = "0.5.7", features = ["csr"] }
log = { version = "0.4"}
ubyte = { version = "0.10.4" }
strum = { version = "0.25.0", features = ["derive"] }

[features]
default = []
Expand Down
2 changes: 1 addition & 1 deletion crates/web-ui/src/components/buckets_chart.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::types::Buckets;
use crate::types::buckets::Buckets;
use chart_js_rs::scatter::Scatter;
use chart_js_rs::{
ChartOptions, ChartScale, Dataset, DatasetDataExt, NoAnnotations, XYDataset, XYPoint,
Expand Down
27 changes: 22 additions & 5 deletions crates/web-ui/src/components/buckets_table.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
use crate::types::Buckets;
use leptos::{component, view, CollectView, IntoView, ReadSignal, SignalWith};
use crate::types::buckets::Buckets;
use crate::types::units::Unit;
use leptos::{
component, view, CollectView, IntoView, ReadSignal, SignalGet, SignalGetUntracked, SignalWith,
};
use leptos_use::{use_intl_number_format, Notation, UseIntlNumberFormatOptions};
use std::time::Duration;
use ubyte::ToByteUnit;

#[component]
pub(crate) fn BucketsTable(buckets: ReadSignal<Buckets>) -> impl IntoView {
let rounded_float_format = use_intl_number_format(
pub(crate) fn BucketsTable(buckets: ReadSignal<Buckets>, unit: ReadSignal<Unit>) -> impl IntoView {
let intl_float_format = use_intl_number_format(
UseIntlNumberFormatOptions::default()
.notation(Notation::Compact)
.minimum_fraction_digits(1)
.maximum_fraction_digits(3),
);

let rounded_float_format = move |val: f64| {
if val.is_infinite() {
return "∞".to_string();
}

match unit.get() {
Unit::Number => intl_float_format.format(val).get_untracked(),
Unit::Bytes => (val.round() as u64).bytes().to_string(),
Unit::Seconds => humantime::format_duration(Duration::from_secs_f64(val)).to_string(),
}
};

let buckets_view = move || {
buckets.with(|buckets| {
buckets
Expand All @@ -19,7 +36,7 @@ pub(crate) fn BucketsTable(buckets: ReadSignal<Buckets>) -> impl IntoView {
view! {
<tr>
<th class="text-start" scope="row">{bucket.number()}</th>
<td class="text-start" scope="col" title={bucket.le()}>{rounded_float_format.format(bucket.le())}</td>
<td class="text-start" scope="col" title={bucket.le()}>{rounded_float_format(bucket.le())}</td>
</tr>
}
})
Expand Down
5 changes: 5 additions & 0 deletions crates/web-ui/src/components/input/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod number_input;
mod select_input;

pub(crate) use number_input::NumberInput;
pub(crate) use select_input::{SelectInput, SelectOption};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ where
.and_then(|v| T::from_str(&v).map_err(|err| err.to_string()))
.and_then(|value| validate_value(value, min, max));

log::debug!("parsed result of `on_change`: {:?}", result);
log::debug!("[number-input] parsed result of `on_change`: {:?}", result);

match result {
Ok(new_value) => {
Expand Down
58 changes: 58 additions & 0 deletions crates/web-ui/src/components/input/select_input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use leptos::ev::Event;
use leptos::{
component, event_target_value, view, Callable, Callback, CollectView, IntoView, ReadSignal,
SignalSet, SignalWith, WriteSignal,
};
use std::fmt::Display;
use std::str::FromStr;

pub(crate) trait SelectOption {
fn label(&self) -> &'static str;
}

#[component]
pub(crate) fn SelectInput<T>(
#[prop(into)] get: ReadSignal<T>,
#[prop(into)] set: WriteSignal<T>,
#[prop(into)] options: Vec<T>,
#[prop(into, optional)] on_change: Option<Callback<Event>>,
#[prop(into)] label: String,
) -> impl IntoView
where
T: SelectOption + Display + PartialEq + FromStr + Copy + 'static,
{
let on_change = move |ev| {
let Ok(new_value) = event_target_value(&ev).parse::<T>() else {
// impossible until somebody changes selector in their browser or wrong impl of `T: FromStr`
log::debug!("[select-input] failed to parse value");
return;
};
log::debug!("[select-input] new value is: {new_value}");

set.set(new_value);
if let Some(callback) = on_change {
callback.call(ev);
}
};

let options_view = options
.into_iter()
.map(|option| {
let is_selected = move || get.with(|v| v == &option);
view! {
<option value=option.to_string() selected=is_selected>
{ option.label() }
</option>
}
})
.collect_view();

view! {
<label class="form-label">
{label}
<select class="form-select" on:change=on_change >
{options_view}
</select>
</label>
}
}
29 changes: 25 additions & 4 deletions crates/web-ui/src/pages/buckets_explorer_page.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::components::buckets_chart::BucketsChart;
use crate::components::buckets_table::BucketsTable;
use crate::components::input::NumberInput;
use crate::types::Buckets;
use crate::components::input::{NumberInput, SelectInput, SelectOption};
use crate::types::buckets::Buckets;
use crate::types::units::Unit;
use leptos::{component, create_signal, view, IntoView, SignalGet, SignalGetUntracked, SignalSet};
use leptos_router::use_query;
use leptos_router::Params;
use strum::IntoEnumIterator;

const DEFAULT_INITIAL_VALUE: f64 = 1.0;
const DEFAULT_FACTOR: f64 = 1.5;
Expand All @@ -31,6 +33,7 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView {
let (factor, set_factor) = create_signal(query.factor.unwrap_or(DEFAULT_FACTOR));
let (buckets_num, set_buckets_num) =
create_signal(query.buckets_num.unwrap_or(DEFAULT_BUCKETS_NUM));
let (unit, set_unit) = create_signal(Unit::Number);

let (buckets, set_buckets) = create_signal(Buckets::calculate(
initial_value.get_untracked(),
Expand All @@ -46,6 +49,8 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView {
))
};

let options: Vec<_> = Unit::iter().collect();

view! {
<div class="container mt-4">
<h1 class="mb-4">Exponential Buckets Explorer</h1>
Expand All @@ -67,17 +72,23 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView {
get=buckets_num set=set_buckets_num on_change=update_buckets_callback
label="Number of buckets" min=1_u32 max=100_u32 />
</div>
<div class="col-md-3 col-sm-auto">
<SelectInput
get=unit set=set_unit options=options
on_change=update_buckets_callback
label="Unit" />
</div>
</div>

<div class="mb-4 row">
{/* Bucket Distribution Table */}
<div class="col-md-3 col-sm-auto text-center">
<h2>Buckets</h2>

<BucketsTable buckets=buckets />
<BucketsTable buckets=buckets unit=unit />
</div>
{/* Chart */}
<div class="col-md-6 col-sm-auto text-center">
<div class="col-md-9 col-sm-auto text-center">
<div>
<h2>Chart</h2>

Expand All @@ -90,3 +101,13 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView {

}
}

impl SelectOption for Unit {
fn label(&self) -> &'static str {
match self {
Unit::Number => "Number",
Unit::Bytes => "Bytes",
Unit::Seconds => "Seconds",
}
}
}
64 changes: 64 additions & 0 deletions crates/web-ui/src/types/buckets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::ops::Deref;

#[derive(Debug, Clone)]
pub(crate) struct Buckets(Vec<Bucket>);

#[derive(Debug, Clone)]
pub(crate) struct Bucket {
number: u32,
le: f64,
}

impl Buckets {
pub(crate) fn calculate(initial_value: f64, factor: f64, num_of_buckets: u32) -> Self {
// Include last bucket, which is open ended
// TODO: use bounded integer type to restrict passing too large values.
// Overflow isn't possible due to validations, but this api is isolated
let num_of_buckets = num_of_buckets
.checked_add(1)
.expect("number of buckets - overflow");
let mut buckets = Vec::with_capacity(num_of_buckets as usize);
let mut next_value = initial_value;

// starting from second bucket, since first one is already added
for bucket_num in 1..num_of_buckets {
buckets.push(Bucket::new(bucket_num, next_value));
next_value *= factor;
}
// last bucket is open ended
buckets.push(Bucket::new(num_of_buckets, f64::INFINITY));

Self(buckets)
}
}

impl Bucket {
fn new(number: u32, le: f64) -> Self {
Self { number, le }
}

pub fn number(&self) -> u32 {
self.number
}

pub fn le(&self) -> f64 {
self.le
}
}

impl Deref for Buckets {
type Target = Vec<Bucket>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl IntoIterator for Buckets {
type Item = Bucket;
type IntoIter = std::vec::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
Loading

0 comments on commit 3dfab4d

Please sign in to comment.