From 9a0ab8ba00597c9e3b35ae15990d20714a5c5295 Mon Sep 17 00:00:00 2001 From: aglkm <39521015+aglkm@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:40:35 +0300 Subject: [PATCH] added sqlite database, enhanced charts, bumped version --- Cargo.lock | 63 ++++++++++- Cargo.toml | 23 ++-- Explorer.toml | 11 +- Rocket.toml | 3 + src/data.rs | 6 +- src/database.rs | 40 +++++++ src/exconfig.rs | 75 ++++++++++++ src/main.rs | 150 +++++++++++++++++++++--- src/requests.rs | 70 +----------- src/worker.rs | 32 +++--- static/styles/style.css | 7 +- templates/base.html.tera | 2 +- templates/stats.html.tera | 233 ++++++++++++++++++++++++++++++++++---- 13 files changed, 579 insertions(+), 136 deletions(-) create mode 100644 src/database.rs create mode 100644 src/exconfig.rs diff --git a/Cargo.lock b/Cargo.lock index eaa258b..81673f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -575,6 +587,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.1.1" @@ -813,7 +837,7 @@ dependencies = [ [[package]] name = "grin-explorer" -version = "0.1.7" +version = "0.1.8" dependencies = [ "anyhow", "chrono", @@ -828,6 +852,7 @@ dependencies = [ "reqwest", "rocket", "rocket_dyn_templates", + "rusqlite", "serde", "serde_json", "shellexpand", @@ -864,6 +889,18 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] name = "hermit-abi" @@ -1158,6 +1195,16 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1936,6 +1983,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.6.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust-ini" version = "0.19.0" diff --git a/Cargo.toml b/Cargo.toml index a6a86f3..e9c786d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,26 @@ [package] name = "grin-explorer" -version = "0.1.7" +version = "0.1.8" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.86" +chrono = "0.4.37" +config = "0.14.0" +either = "1.11.0" +env_logger = "0.11.3" +fs_extra = "1.3.0" +futures = "0.3.30" +humantime = "2.1.0" +lazy_static = "1.4.0" +num-format = "0.4.4" rocket = {version = "0.5.0", features = ["json"]} +rusqlite = "0.32.1" serde = {version = "1.0.198", features = ["derive"]} serde_json = "1.0.111" -num-format = "0.4.4" -fs_extra = "1.3.0" -humantime = "2.1.0" -chrono = "0.4.37" -futures = "0.3.30" -config = "0.14.0" -lazy_static = "1.4.0" shellexpand = "3.1.0" -either = "1.11.0" -anyhow = "1.0.86" -env_logger = "0.11.3" tera_thousands = "0.1.0" [dependencies.reqwest] diff --git a/Explorer.toml b/Explorer.toml index a56e57b..269c728 100644 --- a/Explorer.toml +++ b/Explorer.toml @@ -27,7 +27,13 @@ public_api = "enabled" # List of external node endpoints, used for peer stats. # Comment out if you wish, only local peer stats will be used then. -# external_nodes = ["https://grinnode.live:3413", "https://grincoin.org"] +# external_nodes = ["https://grinnode.live:3413", "https://grincoin.org"] + +# Database path. +# By default, it will be created in the current directory. +# Comment out if you don't want to use sqlite database +database = "database.sqlite" + # Grinnode config # host = "grinnode.live" @@ -35,6 +41,7 @@ public_api = "enabled" # proto = "https" # coingecko_api = "enabled" # public_api = "enabled" +# database = "database.sqlite" # Grincoin config @@ -42,6 +49,7 @@ public_api = "enabled" # proto = "https" # coingecko_api = "enabled" # public_api = "enabled" +# database = "database.sqlite" # Testnet config @@ -54,4 +62,5 @@ public_api = "enabled" # grin_dir = "~/.grin" # coingecko_api = "disabled" # public_api = "enabled" +# database = "database.sqlite" diff --git a/Rocket.toml b/Rocket.toml index caf68a8..4bad97f 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -5,3 +5,6 @@ address = "127.0.0.1" # E.g. Mainnet (8000) and Testnet (8001) instances. # port = 8000 +[default.databases.sqlite_db] +url = "database.sqlite" + diff --git a/src/data.rs b/src/data.rs index 2ea4e0a..1fd236c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -191,6 +191,7 @@ pub struct ExplorerConfig { pub coingecko_api: String, pub public_api: String, pub external_nodes: Vec, + pub database: String, } impl ExplorerConfig { @@ -208,6 +209,7 @@ impl ExplorerConfig { coingecko_api: String::new(), public_api: String::new(), external_nodes: Vec::new(), + database: String::new(), } } } @@ -250,7 +252,7 @@ pub struct Statistics { pub txns: Vec, pub fees: Vec, // UTXOs - pub utxo_count: Vec, + pub utxos: Vec, // Kernels pub kernels: Vec, } @@ -265,7 +267,7 @@ impl Statistics { hashrate: Vec::new(), txns: Vec::new(), fees: Vec::new(), - utxo_count: Vec::new(), + utxos: Vec::new(), kernels: Vec::new(), } } diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..cdf79bf --- /dev/null +++ b/src/database.rs @@ -0,0 +1,40 @@ +use rusqlite::{Connection, Result}; + +pub fn open_db_connection(db_name: &str) -> Result { + let conn = Connection::open(db_name)?; + + Ok(conn) +} + +pub fn create_statistics_table(conn: &Connection) -> Result<()> { + conn.execute( + "CREATE TABLE IF NOT EXISTS statistics ( + id INTEGER PRIMARY KEY, + date TEXT NOT NULL UNIQUE, + hashrate TEXT NOT NULL, + txns TEXT NOT NULL, + fees TEXT NOT NULL, + utxos TEXT NOT NULL, + kernels TEXT NOT NULL + )", + (), // empty list of parameters. + )?; + + Ok(()) +} + +pub fn read_row(conn: &Connection, row_name: &str) -> Result> { + let sql = format!("SELECT {} FROM statistics ORDER BY id", row_name); + let mut stmt = conn.prepare(&sql)?; + + let data_iter = stmt + .query_map([], |row| { + row.get(0) + }).unwrap(); + + // Collect all the results into a vector of strings + let data: Vec = data_iter.collect::, _>>().unwrap(); + + Ok(data) +} + diff --git a/src/exconfig.rs b/src/exconfig.rs new file mode 100644 index 0000000..e736c79 --- /dev/null +++ b/src/exconfig.rs @@ -0,0 +1,75 @@ +use config::Config; +use std::fs; +use lazy_static::lazy_static; + +use crate::data::ExplorerConfig; + + +// Static explorer config structure +lazy_static! { + pub static ref CONFIG: ExplorerConfig = { + let mut cfg = ExplorerConfig::new(); + let toml = Config::builder().add_source(config::File::with_name("Explorer")).build().unwrap(); + + // Mandatory settings + cfg.host = toml.get_string("host").unwrap(); + cfg.proto = toml.get_string("proto").unwrap(); + cfg.coingecko_api = toml.get_string("coingecko_api").unwrap(); + cfg.public_api = toml.get_string("public_api").unwrap(); + + // Optional settings + match toml.get_string("port") { + Ok(v) => cfg.port = v, + Err(_e) => {}, + } + + match toml.get_string("user") { + Ok(v) => cfg.user = v, + Err(_e) => {}, + } + + match toml.get_string("api_secret_path") { + Ok(v) => cfg.api_secret_path = v, + Err(_e) => {}, + } + + match toml.get_string("foreign_api_secret_path") { + Ok(v) => cfg.foreign_api_secret_path = v, + Err(_e) => {}, + } + + match toml.get_string("grin_dir") { + Ok(v) => cfg.grin_dir = v, + Err(_e) => {}, + } + + match toml.get_array("external_nodes") { + Ok(nodes) => { + for endpoint in nodes.clone() { + cfg.external_nodes.push(endpoint.into_string().unwrap()); + } + }, + Err(_e) => {}, + } + + match toml.get_string("database") { + Ok(v) => cfg.database = v, + Err(_e) => {}, + } + + if cfg.api_secret_path.is_empty() == false { + cfg.api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.api_secret_path))).unwrap(); + } + + if cfg.foreign_api_secret_path.is_empty() == false { + cfg.foreign_api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.foreign_api_secret_path))).unwrap(); + } + + if cfg.grin_dir.is_empty() == false { + cfg.grin_dir = format!("{}", shellexpand::tilde(&cfg.grin_dir)); + } + + cfg + }; +} + diff --git a/src/main.rs b/src/main.rs index e182037..e3823ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,9 +13,11 @@ use serde_json::Value; use tera_thousands::separate_with_commas; use crate::data::{Block, Dashboard, Kernel, Output, Statistics, Transactions, OUTPUT_SIZE, KERNEL_SIZE}; -use crate::requests::CONFIG; +use crate::exconfig::CONFIG; mod data; +mod database; +mod exconfig; mod requests; mod worker; @@ -219,24 +221,112 @@ pub async fn search(input: Option<&str>) -> Either { fn stats(statistics: &State>>) -> Template { let data = statistics.lock().unwrap(); + // Get the length of our data vectors (all vectors are the same size) + let len = data.date.len(); + + // Construct chart periods + let mut month = 0; + let mut six_months = 0; + let mut year = 0; + + // Usize type can't be negative, so check the lenght of the vector + if len > 30 { + month = len - 30; + } + if len > (30 * 6) { + six_months = len - (30 * 6); + } + if len > 365 { + year = len - 365; + } + + let mut m_date = data.date.clone(); + let mut m_hashrate = data.hashrate.clone(); + let mut m_txns = data.txns.clone(); + let mut m_fees = data.fees.clone(); + let mut m_utxos = data.utxos.clone(); + let mut m_kernels = data.kernels.clone(); + + // Get stats for a month period + if month > 0 { + m_date = data.date.get(month..).unwrap().to_vec(); + m_hashrate = data.hashrate.get(month..).unwrap().to_vec(); + m_txns = data.txns.get(month..).unwrap().to_vec(); + m_fees = data.fees.get(month..).unwrap().to_vec(); + m_utxos = data.utxos.get(month..).unwrap().to_vec(); + m_kernels = data.kernels.get(month..).unwrap().to_vec(); + } + + let mut sm_date = data.date.clone(); + let mut sm_hashrate = data.hashrate.clone(); + let mut sm_txns = data.txns.clone(); + let mut sm_fees = data.fees.clone(); + let mut sm_utxos = data.utxos.clone(); + let mut sm_kernels = data.kernels.clone(); + + // Get stats for six months period + if six_months > 0 { + sm_date = data.date.get(six_months..).unwrap().to_vec(); + sm_hashrate = data.hashrate.get(six_months..).unwrap().to_vec(); + sm_txns = data.txns.get(six_months..).unwrap().to_vec(); + sm_fees = data.fees.get(six_months..).unwrap().to_vec(); + sm_utxos = data.utxos.get(six_months..).unwrap().to_vec(); + sm_kernels = data.kernels.get(six_months..).unwrap().to_vec(); + } + + let mut y_date = data.date.clone(); + let mut y_hashrate = data.hashrate.clone(); + let mut y_txns = data.txns.clone(); + let mut y_fees = data.fees.clone(); + let mut y_utxos = data.utxos.clone(); + let mut y_kernels = data.kernels.clone(); + + // Get stats for a year period + if year > 0 { + y_date = data.date.get(year..).unwrap().to_vec(); + y_hashrate = data.hashrate.get(year..).unwrap().to_vec(); + y_txns = data.txns.get(year..).unwrap().to_vec(); + y_fees = data.fees.get(year..).unwrap().to_vec(); + y_utxos = data.utxos.get(year..).unwrap().to_vec(); + y_kernels = data.kernels.get(year..).unwrap().to_vec(); + } + Template::render("stats", context! { - route: "stats", - date: data.date.clone(), - user_agent: data.user_agent.clone(), - count: data.count.clone(), - total: data.total, - hashrate: data.hashrate.clone(), - txns: data.txns.clone(), - fees: data.fees.clone(), - utxo_count: data.utxo_count.clone(), - kernels: data.kernels.clone(), + route: "stats", + user_agent: data.user_agent.clone(), + count: data.count.clone(), + total: data.total, + date: data.date.clone(), + hashrate: data.hashrate.clone(), + txns: data.txns.clone(), + fees: data.fees.clone(), + utxos: data.utxos.clone(), + kernels: data.kernels.clone(), + m_date, + m_hashrate, + m_txns, + m_fees, + m_utxos, + m_kernels, + sm_date, + sm_hashrate, + sm_txns, + sm_fees, + sm_utxos, + sm_kernels, + y_date, + y_hashrate, + y_txns, + y_fees, + y_utxos, + y_kernels, output_size: OUTPUT_SIZE, kernel_size: KERNEL_SIZE, }) } -// Rendering Grinflation page. +// Rendering Emission page. #[get("/emission")] fn emission(dashboard: &State>>) -> Template { let data = dashboard.lock().unwrap(); @@ -752,7 +842,36 @@ async fn main() { let mut ready_data = false; let mut ready_stats = false; + let mut ready_db = false; let mut date = "".to_string(); + + // Initializing db and table + if CONFIG.database.is_empty() == false { + info!("initializing db."); + let conn = database::open_db_connection(&CONFIG.database).expect("failed to open database"); + database::create_statistics_table(&conn).expect("failed to create statistics table"); + + let mut s = stats.lock().unwrap(); + let mut d = dash.lock().unwrap(); + + // Reading the database + s.date = database::read_row(&conn, "date").unwrap(); + s.hashrate = database::read_row(&conn, "hashrate").unwrap(); + s.txns = database::read_row(&conn, "txns").unwrap(); + s.fees = database::read_row(&conn, "fees").unwrap(); + s.utxos = database::read_row(&conn, "utxos").unwrap(); + s.kernels = database::read_row(&conn, "kernels").unwrap(); + + // Read utxos right here, because we have it in worker::stats thread launched next day only + if s.utxos.is_empty() == false { + d.utxo_count = s.utxos.get(s.utxos.len() - 1).unwrap().to_string(); + } + + // Get the latest date + if s.date.is_empty() == false { + date = s.date.get(s.date.len() - 1).unwrap().to_string(); + } + } // Collecting main data tokio::spawn(async move { @@ -775,7 +894,7 @@ async fn main() { let date_now = format!("\"{}\"", Utc::now().format("%d-%m-%Y")); - if date.is_empty() || date != date_now { + if date != date_now { date = date_now; let result = worker::stats(dash_clone.clone(), txns_clone.clone(), stats_clone.clone()).await; @@ -784,6 +903,7 @@ async fn main() { Ok(_v) => { if ready_stats == false { ready_stats = true; + ready_db = true; info!("worker::stats ready."); } }, @@ -792,6 +912,10 @@ async fn main() { error!("{}", e); }, } + // Got stats from DB, indicate ready state + } else if ready_db == false && CONFIG.database.is_empty() == false { + info!("worker::stats ready."); + ready_db = true; } tokio::time::sleep(Duration::from_secs(15)).await; diff --git a/src/requests.rs b/src/requests.rs index ac66ec5..e5914f8 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -6,77 +6,11 @@ use fs_extra::dir::get_size; use humantime::format_duration; use std::time::Duration; use chrono::{Utc, DateTime}; -use config::Config; use std::collections::HashMap; -use std::fs; -use lazy_static::lazy_static; -use crate::data::{Block, Dashboard, ExplorerConfig, Kernel, Output, Statistics, Transactions}; +use crate::data::{Block, Dashboard, Kernel, Output, Statistics, Transactions}; use crate::data::{KERNEL_WEIGHT, INPUT_WEIGHT, OUTPUT_WEIGHT, KERNEL_SIZE, INPUT_SIZE, OUTPUT_SIZE}; - - -// Static explorer config structure -lazy_static! { - pub static ref CONFIG: ExplorerConfig = { - let mut cfg = ExplorerConfig::new(); - let toml = Config::builder().add_source(config::File::with_name("Explorer")).build().unwrap(); - - // Mandatory settings - cfg.host = toml.get_string("host").unwrap(); - cfg.proto = toml.get_string("proto").unwrap(); - cfg.coingecko_api = toml.get_string("coingecko_api").unwrap(); - cfg.public_api = toml.get_string("public_api").unwrap(); - - // Optional settings - match toml.get_string("port") { - Ok(v) => cfg.port = v, - Err(_e) => {}, - } - - match toml.get_string("user") { - Ok(v) => cfg.user = v, - Err(_e) => {}, - } - - match toml.get_string("api_secret_path") { - Ok(v) => cfg.api_secret_path = v, - Err(_e) => {}, - } - - match toml.get_string("foreign_api_secret_path") { - Ok(v) => cfg.foreign_api_secret_path = v, - Err(_e) => {}, - } - - match toml.get_string("grin_dir") { - Ok(v) => cfg.grin_dir = v, - Err(_e) => {}, - } - - match toml.get_array("external_nodes") { - Ok(nodes) => { - for endpoint in nodes.clone() { - cfg.external_nodes.push(endpoint.into_string().unwrap()); - } - }, - Err(_e) => {}, - } - - if cfg.api_secret_path.is_empty() == false { - cfg.api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.api_secret_path))).unwrap(); - } - - if cfg.foreign_api_secret_path.is_empty() == false { - cfg.foreign_api_secret = fs::read_to_string(format!("{}", shellexpand::tilde(&cfg.foreign_api_secret_path))).unwrap(); - } - - if cfg.grin_dir.is_empty() == false { - cfg.grin_dir = format!("{}", shellexpand::tilde(&cfg.grin_dir)); - } - - cfg - }; -} +use crate::exconfig::CONFIG; // RPC requests to grin node. diff --git a/src/worker.rs b/src/worker.rs index daf629b..88692d3 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -5,7 +5,8 @@ use crate::data::Block; use crate::data::Dashboard; use crate::data::Statistics; use crate::data::Transactions; - +use crate::database; +use crate::exconfig::CONFIG; use crate::requests; @@ -16,7 +17,7 @@ pub async fn data(dash: Arc>, blocks: Arc>>, let _ = requests::get_mempool(dash.clone()).await?; let _ = requests::get_connected_peers(dash.clone(), stats.clone()).await?; let _ = requests::get_market(dash.clone()).await?; - requests::get_disk_usage(dash.clone())?; + let _ = requests::get_disk_usage(dash.clone())?; let _ = requests::get_mining_stats(dash.clone()).await?; let _ = requests::get_recent_blocks(dash.clone(), blocks.clone()).await?; let _ = requests::get_txn_stats(dash.clone(), txns.clone()).await?; @@ -26,32 +27,37 @@ pub async fn data(dash: Arc>, blocks: Arc>>, // Collecting statistics. pub async fn stats(dash: Arc>, txns: Arc>, stats: Arc>) -> Result<(), anyhow::Error> { + let _ = requests::get_unspent_outputs(dash.clone()).await?; let mut stats = stats.lock().unwrap(); let dash = dash.lock().unwrap(); let txns = txns.lock().unwrap(); - if stats.date.len() == 30 { - stats.date.remove(0); - stats.hashrate.remove(0); - stats.txns.remove(0); - stats.fees.remove(0); - stats.utxo_count.remove(0); - stats.kernels.remove(0); - } - stats.date.push(format!("\"{}\"", Utc::now().format("%d-%m-%Y"))); stats.hashrate.push(dash.hashrate_kgs.clone()); stats.txns.push(txns.period_24h.clone()); stats.fees.push(txns.fees_24h.clone()); - stats.utxo_count.push(dash.utxo_count.clone()); + stats.utxos.push(dash.utxo_count.clone()); + + let mut kernel_count = 0; if dash.kernel_mmr_size.is_empty() == false { - let kernel_count = dash.kernel_mmr_size.parse::().unwrap() / 2; + kernel_count = dash.kernel_mmr_size.parse::().unwrap() / 2; stats.kernels.push(kernel_count.to_string()); } + if CONFIG.database.is_empty() == false { + // Open the database + let conn = database::open_db_connection(&CONFIG.database).expect("failed to open database"); + + //Insert new data into the database + conn.execute( + "INSERT OR IGNORE INTO statistics (date, hashrate, txns, fees, utxos, kernels) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + (&format!("\"{}\"", Utc::now().format("%d-%m-%Y")), &dash.hashrate_kgs.clone(), &txns.period_24h.clone(), &txns.fees_24h.clone(), &dash.utxo_count.clone(), &kernel_count.to_string()), + )?; + } + Ok(()) } diff --git a/static/styles/style.css b/static/styles/style.css index 2822624..7284ca3 100644 --- a/static/styles/style.css +++ b/static/styles/style.css @@ -114,10 +114,9 @@ footer { background-color: silver !important; } -.dark-mode button:focus { outline:0; } - -.dark-mode button:hover, -.dark-mode button:active { +.dark-mode button:hover, +.dark-mode button:focus, +.dark-mode .btn.active { color: silver !important; } diff --git a/templates/base.html.tera b/templates/base.html.tera index c52c112..aa8f418 100644 --- a/templates/base.html.tera +++ b/templates/base.html.tera @@ -294,7 +294,7 @@
- v.0.1.7 + v.0.1.8 Search diff --git a/templates/stats.html.tera b/templates/stats.html.tera index 2c391b7..9c13a86 100644 --- a/templates/stats.html.tera +++ b/templates/stats.html.tera @@ -19,6 +19,12 @@
TRANSACTIONS & FEES
+
+ + + + +
@@ -28,6 +34,12 @@
HASHRATE
+
+ + + + +
@@ -37,6 +49,12 @@
UNSPENT OUTPUTS
+
+ + + + +
@@ -46,6 +64,12 @@
KERNELS
+
+ + + + +
@@ -92,7 +116,7 @@ plugins: { legend: { display: true - }, + } }, scales: { x: { @@ -109,24 +133,28 @@ }; var data = { - labels: {{ date }}, + labels: {{ m_date }}, datasets: [ { label: 'Transactions', - data: {{ txns }}, + data: {{ m_txns }}, fill: false, - tension: 0.1 + tension: 0.1, + radius: 0, + hoverRadius: 4 }, { label: 'Fees', - data: {{ fees }}, + data: {{ m_fees }}, fill: false, - tension: 0.1 + tension: 0.1, + radius: 0, + hoverRadius: 4 } ] }; - new Chart(document.getElementById("2"), { + const txnChart = new Chart(document.getElementById("2"), { type: 'line', data: data, options: options @@ -147,7 +175,7 @@ plugins: { legend: { display: false - }, + } }, scales: { x: { @@ -164,18 +192,20 @@ }; var data = { - labels: {{ date }}, + labels: {{ m_date }}, datasets: [{ label: 'Hashrate (kG/s)', - data: {{ hashrate }}, + data: {{ m_hashrate }}, fill: true, borderColor: "#b25110", backgroundColor: gradient_hash, - tension: 0.1 + tension: 0.1, + radius: 0, + hoverRadius: 4 }] }; - new Chart(document.getElementById("3"), { + const hashChart = new Chart(document.getElementById("3"), { type: 'line', data: data, options: options @@ -224,20 +254,22 @@ }; var data = { - labels: {{ date }}, + labels: {{ m_date }}, datasets: [ { label: 'Unspent Outputs', - data: {{ utxo_count }}, + data: {{ m_utxos }}, fill: true, borderColor: "#b25110", backgroundColor: gradient_utxo, - tension: 0.1 + tension: 0.1, + radius: 0, + hoverRadius: 4 } ] }; - new Chart(document.getElementById("4"), { + const utxoChart = new Chart(document.getElementById("4"), { type: 'line', data: data, options: options @@ -286,27 +318,184 @@ }; var data = { - labels: {{ date }}, + labels: {{ m_date }}, datasets: [ { label: 'Kernels', - data: {{ kernels }}, + data: {{ m_kernels }}, fill: true, borderColor: "#b25110", backgroundColor: gradient_utxo, - tension: 0.1 + tension: 0.1, + radius: 0, + hoverRadius: 4 } ] }; - new Chart(document.getElementById("5"), { + const kerChart = new Chart(document.getElementById("5"), { type: 'line', data: data, options: options }); - - + + // Fill Transactions&Fees chart based on period + function txnTimePeriod(period) { + if(period.value == 'month') { + txnChart.data.labels = {{ m_date }}; + txnChart.data.datasets[0].data = {{ m_txns }}; + txnChart.data.datasets[1].data = {{ m_fees }}; + } + if(period.value == 'sixmonths') { + txnChart.data.labels = {{ sm_date }}; + txnChart.data.datasets[0].data = {{ sm_txns }}; + txnChart.data.datasets[1].data = {{ sm_fees }}; + } + + if(period.value == 'year') { + txnChart.data.labels = {{ y_date }}; + txnChart.data.datasets[0].data = {{ y_txns }}; + txnChart.data.datasets[1].data = {{ y_fees }}; + } + + if(period.value == 'all') { + txnChart.data.labels = {{ date }}; + txnChart.data.datasets[0].data = {{ txns }}; + txnChart.data.datasets[1].data = {{ fees }}; + } + + txnChart.update(); + } + + // Fill Hashrate chart based on period + function hashTimePeriod(period) { + if(period.value == 'month') { + hashChart.data.labels = {{ m_date }}; + hashChart.data.datasets[0].data = {{ m_hashrate }}; + } + + if(period.value == 'sixmonths') { + hashChart.data.labels = {{ sm_date }}; + hashChart.data.datasets[0].data = {{ sm_hashrate }}; + } + + if(period.value == 'year') { + hashChart.data.labels = {{ y_date }}; + hashChart.data.datasets[0].data = {{ y_hashrate }}; + } + + if(period.value == 'all') { + hashChart.data.labels = {{ date }}; + hashChart.data.datasets[0].data = {{ hashrate }}; + } + + hashChart.update(); + } + + // Fill Utxo chart based on period + function utxoTimePeriod(period) { + if(period.value == 'month') { + utxoChart.data.labels = {{ m_date }}; + utxoChart.data.datasets[0].data = {{ m_utxos }}; + } + + if(period.value == 'sixmonths') { + utxoChart.data.labels = {{ sm_date }}; + utxoChart.data.datasets[0].data = {{ sm_utxos }}; + } + + if(period.value == 'year') { + utxoChart.data.labels = {{ y_date }}; + utxoChart.data.datasets[0].data = {{ y_utxos }}; + } + + if(period.value == 'all') { + utxoChart.data.labels = {{ date }}; + utxoChart.data.datasets[0].data = {{ utxos }}; + } + + utxoChart.update(); + } + + // Fill Kernels chart based on period + function kerTimePeriod(period) { + if(period.value == 'month') { + kerChart.data.labels = {{ m_date }}; + kerChart.data.datasets[0].data = {{ m_kernels }}; + } + + if(period.value == 'sixmonths') { + kerChart.data.labels = {{ sm_date }}; + kerChart.data.datasets[0].data = {{ sm_kernels }}; + } + + if(period.value == 'year') { + kerChart.data.labels = {{ y_date }}; + kerChart.data.datasets[0].data = {{ y_kernels }}; + } + + if(period.value == 'all') { + kerChart.data.labels = {{ date }}; + kerChart.data.datasets[0].data = {{ kernels }}; + } + + kerChart.update(); + } + + // + // Code to toogle .active class on period switch buttons + // + + // Get the container element + var txnBtnContainer = document.getElementById("txnBtnGroup"); + var hashBtnContainer = document.getElementById("hashBtnGroup"); + var utxoBtnContainer = document.getElementById("utxoBtnGroup"); + var kerBtnContainer = document.getElementById("kerBtnGroup"); + + // Get all buttons with class="btn" inside the container + var txnBtns = txnBtnContainer.getElementsByClassName("btn"); + var hashBtns = hashBtnContainer.getElementsByClassName("btn"); + var utxoBtns = utxoBtnContainer.getElementsByClassName("btn"); + var kerBtns = kerBtnContainer.getElementsByClassName("btn"); + + // Loop through the buttons and add the active class to the current/clicked button + for (var i = 0; i < txnBtns.length; i++) { + txnBtns[i].addEventListener("click", function() { + var current = txnBtnContainer.getElementsByClassName("active"); + current[0].className = current[0].className.replace(" active", ""); + this.className += " active"; + }); + } + + // Loop through the buttons and add the active class to the current/clicked button + for (var i = 0; i < hashBtns.length; i++) { + hashBtns[i].addEventListener("click", function() { + var current = hashBtnContainer.getElementsByClassName("active"); + current[0].className = current[0].className.replace(" active", ""); + this.className += " active"; + }); + } + + // Loop through the buttons and add the active class to the current/clicked button + for (var i = 0; i < utxoBtns.length; i++) { + utxoBtns[i].addEventListener("click", function() { + var current = utxoBtnContainer.getElementsByClassName("active"); + current[0].className = current[0].className.replace(" active", ""); + this.className += " active"; + }); + } + + // Loop through the buttons and add the active class to the current/clicked button + for (var i = 0; i < kerBtns.length; i++) { + kerBtns[i].addEventListener("click", function() { + var current = kerBtnContainer.getElementsByClassName("active"); + current[0].className = current[0].className.replace(" active", ""); + this.className += " active"; + }); + } + + {% endblock %}