feat: first functional version
This commit is contained in:
parent
bfcafecfea
commit
59d9d0fb37
19 changed files with 3076 additions and 1 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/dist
|
||||||
2419
Cargo.lock
generated
Normal file
2419
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
30
Cargo.toml
Normal file
30
Cargo.toml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "hypergeometric-calc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Felipe Contreras Salinas <felipe@bstr.cl>"]
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
console_log = "1.0.0"
|
||||||
|
fluent-templates = "0.13.0"
|
||||||
|
leptos = { version = "0.7.7", features = ["csr", "tracing"] }
|
||||||
|
leptos-fluent = "0.2.9"
|
||||||
|
leptos_meta = { version = "0.7.7" }
|
||||||
|
leptos_router = { version = "0.7.7" }
|
||||||
|
log = "0.4.26"
|
||||||
|
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wasm-bindgen = "0.2.100"
|
||||||
|
wasm-bindgen-test = "0.3.50"
|
||||||
|
web-sys = { version = "0.3.77", features = ["Document", "Window"] }
|
||||||
|
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 'z'
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
32
README.md
32
README.md
|
|
@ -1,3 +1,33 @@
|
||||||
# hypergeometric-calc
|
# hypergeometric-calc
|
||||||
|
Web calculator for Hypergeometric Distribution built using [Leptos].
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
You will need to install the Rust toolchain. We recommend to do using [Rustup]. Once
|
||||||
|
you do that, we will need to install the WASM target
|
||||||
|
```sh
|
||||||
|
rustup target add wasm32-unknown-unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
We will also use the [Trunk] tool to run and build our project.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
To develop the project, run
|
||||||
|
```sh
|
||||||
|
trunk serve --port 3000 --open
|
||||||
|
```
|
||||||
|
which, will open the app in your default browser at `http://localhost:3000`.
|
||||||
|
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
To build the project for release, use the command
|
||||||
|
```sh
|
||||||
|
trunk build --release
|
||||||
|
```
|
||||||
|
This will output the files necessary to run your app into the `dist` folder; you can then use any static site host to serve these files.
|
||||||
|
|
||||||
|
[Leptos]: https://github.com/leptos-rs/leptos
|
||||||
|
[Rustup]: https://rustup.rs/
|
||||||
|
[Trunk]: https://github.com/trunk-rs/trunk
|
||||||
|
|
||||||
Web calculator for Hypergeometric Distribution
|
|
||||||
60
Trunk.toml
Normal file
60
Trunk.toml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
[build]
|
||||||
|
# The index HTML file to drive the bundling process.
|
||||||
|
target = "index.html"
|
||||||
|
# Build in release mode.
|
||||||
|
release = false
|
||||||
|
# Use a custom cargo profile. Overrides the default chosen by cargo. Ignored if the 'index.html' has one configured.
|
||||||
|
# cargo_profile = ""
|
||||||
|
# The output dir for all final assets.
|
||||||
|
dist = "dist"
|
||||||
|
# The public URL from which assets are to be served.
|
||||||
|
public_url = "/"
|
||||||
|
# Whether to include hash values in the output file names.
|
||||||
|
filehash = true
|
||||||
|
# Whether to inject scripts (and module preloads) into the finalized output.
|
||||||
|
inject_scripts = true
|
||||||
|
# Run without network access
|
||||||
|
# offline = false
|
||||||
|
# Require Cargo.lock and cache are up to date
|
||||||
|
# frozen = false
|
||||||
|
# Require Cargo.lock is up to date
|
||||||
|
# locked = false
|
||||||
|
# Control minification
|
||||||
|
# minify = "never" # can be one of: never, on_release, always
|
||||||
|
# Allow disabling sub-resource integrity (SRI)
|
||||||
|
# no_sri = false
|
||||||
|
# An optional cargo profile to use
|
||||||
|
# cargo_profile = "release-trunk"
|
||||||
|
|
||||||
|
[watch]
|
||||||
|
# Paths to watch. The `build.target`'s parent folder is watched by default.
|
||||||
|
watch = []
|
||||||
|
# Paths to ignore.
|
||||||
|
ignore = []
|
||||||
|
|
||||||
|
[serve]
|
||||||
|
# The address to serve on.
|
||||||
|
addresses = ["127.0.0.1"]
|
||||||
|
# The port to serve on.
|
||||||
|
port = 3000
|
||||||
|
# Aliases to serve, typically found in an /etc/hosts file.
|
||||||
|
# aliases = ["http://localhost.mywebsite.com"]
|
||||||
|
# Disable the reverse DNS lookup during startup
|
||||||
|
# disable_address_lookup = false
|
||||||
|
# Open a browser tab once the initial build is complete.
|
||||||
|
open = false
|
||||||
|
# Whether to disable fallback to index.html for missing files.
|
||||||
|
# no_spa = false
|
||||||
|
# Disable auto-reload of the web app.
|
||||||
|
# no_autoreload = false
|
||||||
|
# Disable error reporting
|
||||||
|
# no_error_reporting = false
|
||||||
|
# Additional headers set for responses.
|
||||||
|
# headers = { "test-header" = "header value", "test-header2" = "header value 2" }
|
||||||
|
# Protocol used for autoreload WebSockets connection.
|
||||||
|
# ws_protocol = "ws"
|
||||||
|
# The certificate/private key pair to use for TLS, which is enabled if both are set.
|
||||||
|
# tls_key_path = "self_signed_certs/key.pem"
|
||||||
|
# tls_cert_path = "self_signed_certs/cert.pem"
|
||||||
|
# Additional headers to send. NOTE: header names must be valid HTTP headers.
|
||||||
|
# headers = { "X-Foo" = "bar" }
|
||||||
18
index.html
Normal file
18
index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- Add a plain CSS file: see https://trunkrs.dev/assets/#css -->
|
||||||
|
<!-- If using Tailwind with Leptos CSR, see https://trunkrs.dev/assets/#tailwind instead-->
|
||||||
|
<link data-trunk rel="scss" href="public/styles.scss" />
|
||||||
|
|
||||||
|
<!-- Include favicon in dist output: see https://trunkrs.dev/assets/#icon -->
|
||||||
|
<link data-trunk rel="icon" href="public/favicon.ico" />
|
||||||
|
|
||||||
|
<!-- include support for `wasm-bindgen --weak-refs` - see: https://rustwasm.github.io/docs/wasm-bindgen/reference/weak-references.html -->
|
||||||
|
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1
locales/en/main.ftl
Normal file
1
locales/en/main.ftl
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
not-found = We couldn't find that page.
|
||||||
1
locales/es/main.ftl
Normal file
1
locales/es/main.ftl
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
not-found = No pudimos encontrar esta página.
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
52
public/styles.scss
Normal file
52
public/styles.scss
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/* --------------------- Open Props --------------------------- */
|
||||||
|
|
||||||
|
/* the props */
|
||||||
|
@import "https://unpkg.com/open-props";
|
||||||
|
|
||||||
|
/* optional imports that use the props */
|
||||||
|
@import "https://unpkg.com/open-props/normalize.min.css";
|
||||||
|
@import "https://unpkg.com/open-props/buttons.min.css";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
button {
|
||||||
|
margin: var(--size-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
body > picture,
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem;
|
||||||
|
}
|
||||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
243
src/calc.rs
Normal file
243
src/calc.rs
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
//! Module responsible for the actual math.
|
||||||
|
|
||||||
|
use std::{collections::HashMap, iter::repeat};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct HyperGeometricProb {
|
||||||
|
pub exactly: f64,
|
||||||
|
pub less_than: f64,
|
||||||
|
pub less_or_equal: f64,
|
||||||
|
pub greater_than: f64,
|
||||||
|
pub greater_or_equal: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hyper_geometric(
|
||||||
|
population_size: u8,
|
||||||
|
successes: u8,
|
||||||
|
sample_size: u8,
|
||||||
|
sample_successes: u8,
|
||||||
|
) -> Option<HyperGeometricProb> {
|
||||||
|
if successes > population_size
|
||||||
|
|| sample_size > population_size
|
||||||
|
|| sample_successes > sample_size
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let exactly =
|
||||||
|
hyper_geometric_exactly(population_size, successes, sample_size, sample_successes);
|
||||||
|
let (less_than, less_or_equal, greater_or_equal, greater_than) =
|
||||||
|
if sample_successes < sample_size / 2 {
|
||||||
|
let less_than: f64 = (1..sample_size)
|
||||||
|
.map(|i| hyper_geometric_exactly(population_size, successes, sample_size, i))
|
||||||
|
.sum();
|
||||||
|
let less_or_equal = less_than + exactly;
|
||||||
|
let greater_or_equal = 1.0 - less_than;
|
||||||
|
let greater_than = 1.0 - less_or_equal;
|
||||||
|
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||||
|
} else {
|
||||||
|
let greater_than: f64 = (sample_successes + 1..=sample_size)
|
||||||
|
.map(|i| hyper_geometric_exactly(population_size, successes, sample_size, i))
|
||||||
|
.sum();
|
||||||
|
let greater_or_equal = greater_than + exactly;
|
||||||
|
let less_or_equal = 1.0 - greater_than;
|
||||||
|
let less_than = 1.0 - greater_or_equal;
|
||||||
|
(less_than, less_or_equal, greater_or_equal, greater_than)
|
||||||
|
};
|
||||||
|
Some(HyperGeometricProb {
|
||||||
|
exactly,
|
||||||
|
less_than,
|
||||||
|
less_or_equal,
|
||||||
|
greater_than,
|
||||||
|
greater_or_equal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the probability of drawing exactly `sample_successes` in a sample of `sample_size`
|
||||||
|
/// from a population of size `population_size` and `successes` total successes in the population.
|
||||||
|
///
|
||||||
|
/// The formula is choose(successes, sample_successes) * choose(population_size - successes,
|
||||||
|
/// sample_size - sample_successes) / choose(population_size, sample_size)
|
||||||
|
fn hyper_geometric_exactly(
|
||||||
|
population_size: u8,
|
||||||
|
successes: u8,
|
||||||
|
sample_size: u8,
|
||||||
|
sample_successes: u8,
|
||||||
|
) -> f64 {
|
||||||
|
if population_size == successes {
|
||||||
|
return if sample_successes == sample_size {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if successes == 0 {
|
||||||
|
return if sample_successes == 0 { 1.0 } else { 0.0 };
|
||||||
|
}
|
||||||
|
// On top we have: successes!, (population_size - successes)!, sample_size! and
|
||||||
|
// (population_size - sample_size)!
|
||||||
|
let top_factors = (1..=successes)
|
||||||
|
.chain(1..=(population_size - successes))
|
||||||
|
.chain(1..=sample_size)
|
||||||
|
.chain(1..=(population_size - sample_size))
|
||||||
|
.flat_map(|n| factorize(n))
|
||||||
|
.fold(HashMap::<u8, u8>::new(), |mut counts, i| {
|
||||||
|
*counts.entry(i).or_default() += 1;
|
||||||
|
counts
|
||||||
|
});
|
||||||
|
|
||||||
|
// On bottom we have: sample_successes!, (successes - sample_successes)!
|
||||||
|
// (sample_size - sample_successes)!, (population_size - successes - sample_size + sample_successes)!
|
||||||
|
// and population_size!
|
||||||
|
let bot_factors = (1..=sample_successes)
|
||||||
|
.chain(1..=(successes - sample_successes))
|
||||||
|
.chain(1..=(sample_size - sample_successes))
|
||||||
|
.chain(
|
||||||
|
1..=((population_size as u16 + sample_successes as u16
|
||||||
|
- successes as u16
|
||||||
|
- sample_size as u16) as u8),
|
||||||
|
)
|
||||||
|
.chain(1..=population_size)
|
||||||
|
.flat_map(|n| factorize(n))
|
||||||
|
.fold(HashMap::<u8, u8>::new(), |mut counts, i| {
|
||||||
|
counts.entry(i).and_modify(|count| *count += 1).or_insert(1);
|
||||||
|
counts
|
||||||
|
});
|
||||||
|
|
||||||
|
let (top_factors, bot_factors) = simplify(top_factors, bot_factors);
|
||||||
|
|
||||||
|
let top_product: f64 = top_factors
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(f, count)| repeat(f).take(count as usize))
|
||||||
|
.map(|f| f as f64)
|
||||||
|
.product();
|
||||||
|
|
||||||
|
let bot_product: f64 = bot_factors
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(f, count)| repeat(f).take(count as usize))
|
||||||
|
.map(|f| f as f64)
|
||||||
|
.product();
|
||||||
|
|
||||||
|
top_product / bot_product
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simplify(
|
||||||
|
mut top_factors: HashMap<u8, u8>,
|
||||||
|
mut bot_factors: HashMap<u8, u8>,
|
||||||
|
) -> (HashMap<u8, u8>, HashMap<u8, u8>) {
|
||||||
|
for (factor, count) in &mut top_factors {
|
||||||
|
if let Some(other_count) = bot_factors.get_mut(factor) {
|
||||||
|
let min_count = *count.min(other_count);
|
||||||
|
*other_count -= min_count;
|
||||||
|
*count -= min_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (factor, count) in &mut bot_factors {
|
||||||
|
if let Some(other_count) = top_factors.get_mut(factor) {
|
||||||
|
let min_count = *count.min(other_count);
|
||||||
|
*other_count -= min_count;
|
||||||
|
*count -= min_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let top_factors = top_factors
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, count)| *count > 0)
|
||||||
|
.collect();
|
||||||
|
let bot_factors = bot_factors
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, count)| *count > 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(top_factors, bot_factors)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRIMES: &[u8] = &[2, 3, 5, 7, 11, 13];
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FactorIter<'a> {
|
||||||
|
/// remainder
|
||||||
|
n: u8,
|
||||||
|
/// current factor
|
||||||
|
f: Option<u8>,
|
||||||
|
/// Iterator
|
||||||
|
it: std::slice::Iter<'a, u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for FactorIter<'_> {
|
||||||
|
type Item = u8;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.n <= 1 || self.f.is_none() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
loop {
|
||||||
|
match self.f {
|
||||||
|
Some(f) => {
|
||||||
|
if self.n % f == 0 {
|
||||||
|
self.n = self.n / f;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
self.f = self.it.next().copied();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we stopped at a factor, we return it.
|
||||||
|
// If we exhausted the factors, we return the remainder, as it will be a prime (we
|
||||||
|
// checked primes less than sqrt(MAX).
|
||||||
|
match self.f {
|
||||||
|
Some(f) => Some(f),
|
||||||
|
None => Some(self.n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn factorize(n: u8) -> FactorIter<'static> {
|
||||||
|
let mut it = PRIMES.iter();
|
||||||
|
let f = it.next().copied();
|
||||||
|
|
||||||
|
FactorIter { n, f, it }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::calc::hyper_geometric_exactly;
|
||||||
|
|
||||||
|
use super::factorize;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_factorize() {
|
||||||
|
let factors = factorize(54);
|
||||||
|
let product: u8 = factors.product();
|
||||||
|
assert_eq!(product, 54);
|
||||||
|
|
||||||
|
let factors = factorize(10);
|
||||||
|
let product: u8 = factors.product();
|
||||||
|
assert_eq!(product, 10);
|
||||||
|
|
||||||
|
let factors = factorize(133);
|
||||||
|
let product: u8 = factors.product();
|
||||||
|
assert_eq!(product, 133);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hypergeometric_exact_all_successes() {
|
||||||
|
assert_eq!(hyper_geometric_exactly(10, 10, 5, 5), 1.0);
|
||||||
|
assert_eq!(hyper_geometric_exactly(10, 10, 5, 4), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hypergeometric_exact_no_successes() {
|
||||||
|
assert_eq!(hyper_geometric_exactly(10, 0, 5, 0), 1.0);
|
||||||
|
assert_eq!(hyper_geometric_exactly(10, 0, 5, 1), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hypergeometric_exact() {
|
||||||
|
assert_eq!(hyper_geometric_exactly(10, 3, 5, 2), 5.0 / 12.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/components/calculator.rs
Normal file
109
src/components/calculator.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
//! Hypergeometric Distribution Calculator
|
||||||
|
use leptos::html::ElementChild;
|
||||||
|
use leptos::prelude::{signal, Get, GlobalAttributes, OnTargetAttribute, PropAttribute, Set};
|
||||||
|
use leptos::{component, view, IntoView};
|
||||||
|
|
||||||
|
use crate::calc::hyper_geometric;
|
||||||
|
|
||||||
|
/// A parameterized incrementing button
|
||||||
|
#[component]
|
||||||
|
pub fn Calculator() -> impl IntoView {
|
||||||
|
let (population, set_population) = signal(0u8);
|
||||||
|
let (successes, set_successes) = signal(0u8);
|
||||||
|
let (sample, set_sample) = signal(0u8);
|
||||||
|
let (sample_successes, set_sample_successes) = signal(0u8);
|
||||||
|
let result = move || {
|
||||||
|
hyper_geometric(
|
||||||
|
population.get(),
|
||||||
|
successes.get(),
|
||||||
|
sample.get(),
|
||||||
|
sample_successes.get(),
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
<label for="population">Population Size</label>
|
||||||
|
<input
|
||||||
|
id="population"
|
||||||
|
type="number"
|
||||||
|
min=0
|
||||||
|
prop:value=population
|
||||||
|
on:input:target=move |ev| {
|
||||||
|
set_population.set(ev.target().value().parse().unwrap_or_default())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="successes">Successes in Population</label>
|
||||||
|
<input
|
||||||
|
id="successes"
|
||||||
|
type="number"
|
||||||
|
min=0
|
||||||
|
prop:max=population
|
||||||
|
prop:value=successes
|
||||||
|
on:input:target=move |ev| {
|
||||||
|
set_successes.set(ev.target().value().parse().unwrap_or_default())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="sample">Sample Size</label>
|
||||||
|
<input
|
||||||
|
id="sample"
|
||||||
|
type="number"
|
||||||
|
min=0
|
||||||
|
prop:max=population
|
||||||
|
prop:value=sample
|
||||||
|
on:input:target=move |ev| {
|
||||||
|
set_sample.set(ev.target().value().parse().unwrap_or_default())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="sample_successes">Successes in Sample</label>
|
||||||
|
<input
|
||||||
|
id="sample_successes"
|
||||||
|
type="number"
|
||||||
|
min=0
|
||||||
|
prop:max=sample
|
||||||
|
prop:value=sample_successes
|
||||||
|
on:input:target=move |ev| {
|
||||||
|
set_sample_successes.set(ev.target().value().parse().unwrap_or_default())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>"P(X = "</span>
|
||||||
|
<span>{sample_successes}</span>
|
||||||
|
<span>"): "</span>
|
||||||
|
<span>{move|| result().exactly}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>"P(X < "</span>
|
||||||
|
<span>{sample_successes}</span>
|
||||||
|
<span>"): "</span>
|
||||||
|
<span>{move|| result().less_than}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>"P(X ≤ "</span>
|
||||||
|
<span>{sample_successes}</span>
|
||||||
|
<span>"): "</span>
|
||||||
|
<span>{move|| result().less_or_equal}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>"P(X > "</span>
|
||||||
|
<span>{sample_successes}</span>
|
||||||
|
<span>"): "</span>
|
||||||
|
<span>{move|| result().greater_than}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>"P(X ≥ "</span>
|
||||||
|
<span>{sample_successes}</span>
|
||||||
|
<span>"): "</span>
|
||||||
|
<span>{move|| result().greater_or_equal}</span>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/components/mod.rs
Normal file
1
src/components/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod calculator;
|
||||||
45
src/lib.rs
Normal file
45
src/lib.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use fluent_templates::static_loader;
|
||||||
|
use leptos::prelude::{AddAnyAttr, IntoAttribute};
|
||||||
|
use leptos::{component, view, IntoView};
|
||||||
|
use leptos_meta::*;
|
||||||
|
use leptos_router::{components::*, path};
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
mod calc;
|
||||||
|
mod components;
|
||||||
|
mod pages;
|
||||||
|
|
||||||
|
// Top-Level pages
|
||||||
|
use crate::pages::home::Home;
|
||||||
|
|
||||||
|
// Localization
|
||||||
|
static_loader! {
|
||||||
|
pub static TRANSLATIONS = {
|
||||||
|
locales: "./locales",
|
||||||
|
fallback_language: "en",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An app router which renders the homepage and handles 404's
|
||||||
|
#[component]
|
||||||
|
pub fn App() -> impl IntoView {
|
||||||
|
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||||
|
provide_meta_context();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Html attr:lang="en" attr:dir="ltr" attr:data-theme="light" />
|
||||||
|
|
||||||
|
// sets the document title
|
||||||
|
<Title text="Welcome to Leptos CSR" />
|
||||||
|
|
||||||
|
// injects metadata in the <head> of the page
|
||||||
|
<Meta charset="UTF-8" />
|
||||||
|
<Meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<Router>
|
||||||
|
<Routes fallback=|| view! { NotFound }>
|
||||||
|
<Route path=path!("/") view=Home />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main.rs
Normal file
12
src/main.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
use hypergeometric_calc::App;
|
||||||
|
use leptos::{mount::mount_to_body, view};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// set up logging
|
||||||
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
mount_to_body(|| {
|
||||||
|
view! { <App /> }
|
||||||
|
})
|
||||||
|
}
|
||||||
40
src/pages/home.rs
Normal file
40
src/pages/home.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
use leptos::attr::global::ClassAttribute;
|
||||||
|
use leptos::error::ErrorBoundary;
|
||||||
|
use leptos::html::ElementChild;
|
||||||
|
use leptos::prelude::{CollectView, Get};
|
||||||
|
use leptos::{component, view, IntoView};
|
||||||
|
|
||||||
|
use crate::components::calculator::Calculator;
|
||||||
|
|
||||||
|
/// Default Home Page
|
||||||
|
#[component]
|
||||||
|
pub fn Home() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<ErrorBoundary fallback=|errors| {
|
||||||
|
view! {
|
||||||
|
<h1>"Uh oh! Something went wrong!"</h1>
|
||||||
|
|
||||||
|
<p>"Errors: "</p>
|
||||||
|
// Render a list of errors as strings - good for development purposes
|
||||||
|
<ul>
|
||||||
|
{move || {
|
||||||
|
errors
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, e)| view! { <li>{e.to_string()}</li> })
|
||||||
|
.collect_view()
|
||||||
|
}}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<div class="container">
|
||||||
|
<h1>"Hypergeometric Distribution Calculator"</h1>
|
||||||
|
|
||||||
|
<div class="calculator">
|
||||||
|
<Calculator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/pages/mod.rs
Normal file
2
src/pages/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod home;
|
||||||
|
pub mod not_found;
|
||||||
8
src/pages/not_found.rs
Normal file
8
src/pages/not_found.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use leptos::html::ElementChild;
|
||||||
|
use leptos::{component, view, IntoView};
|
||||||
|
|
||||||
|
/// 404 Not Found Page
|
||||||
|
#[component]
|
||||||
|
pub fn NotFound() -> impl IntoView {
|
||||||
|
view! { <h1>"We couldn't find that page."</h1> }
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue