feat: committing current state

Committing current state, which is a simple full
screen plot with some basic features. It compiles
for both native and web. There's a basic generic
datasource impl. It currently plots random data for demo.
This commit is contained in:
əlemi 2022-06-03 02:03:37 +02:00
parent c256fec42f
commit 6653024bad
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
17 changed files with 2046 additions and 0 deletions

25
Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "plotter"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "plotter_bin"
path = "src/main.rs"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
rand = "0.8"
chrono = { version = "0.4", features = ["wasmbind"] }
eframe = { version = "0.18", features = ["persistence"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ehttp = "0.2.0"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"

77
build_web.sh Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -eu
script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
cd "$script_path"
OPEN=false
FAST=false
while test $# -gt 0; do
case "$1" in
-h|--help)
echo "build_web.sh [--fast] [--open]"
echo " --fast: skip optimization step"
echo " --open: open the result in a browser"
exit 0
;;
--fast)
shift
FAST=true
;;
--open)
shift
OPEN=true
;;
*)
break
;;
esac
done
# ./setup_web.sh # <- call this first!
FOLDER_NAME=${PWD##*/}
CRATE_NAME=$FOLDER_NAME # assume crate name is the same as the folder name
CRATE_NAME_SNAKE_CASE="${CRATE_NAME//-/_}" # for those who name crates with-kebab-case
# This is required to enable the web_sys clipboard API which egui_web uses
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
export RUSTFLAGS=--cfg=web_sys_unstable_apis
# Clear output from old stuff:
rm -f "docs/${CRATE_NAME_SNAKE_CASE}_bg.wasm"
echo "Building rust…"
BUILD=release
cargo build -p "${CRATE_NAME}" --release --lib --target wasm32-unknown-unknown
# Get the output directory (in the workspace it is in another location)
TARGET=$(cargo metadata --format-version=1 | jq --raw-output .target_directory)
echo "Generating JS bindings for wasm…"
TARGET_NAME="${CRATE_NAME_SNAKE_CASE}.wasm"
WASM_PATH="${TARGET}/wasm32-unknown-unknown/${BUILD}/${TARGET_NAME}"
wasm-bindgen "${WASM_PATH}" --out-dir docs --no-modules --no-typescript
if [[ "${FAST}" == false ]]; then
echo "Optimizing wasm…"
# to get wasm-opt: apt/brew/dnf install binaryen
wasm-opt "docs/${CRATE_NAME}_bg.wasm" -O2 --fast-math -o "docs/${CRATE_NAME}_bg.wasm" # add -g to get debug symbols
fi
echo "Finished: docs/${CRATE_NAME_SNAKE_CASE}.wasm"
if [[ "${OPEN}" == true ]]; then
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux, ex: Fedora
xdg-open http://localhost:8080/index.html
elif [[ "$OSTYPE" == "msys" ]]; then
# Windows
start http://localhost:8080/index.html
else
# Darwin/MacOS, or something else
open http://localhost:8080/index.html
fi
fi

3
docs/README.md Normal file
View file

@ -0,0 +1,3 @@
This folder contains the files required for the web app.
The reason the folder is called "docs" is because that is the name that GitHub requires in order to host a web page from the `master` branch of a repository. You can test the `eframe_template` at <https://emilk.github.io/eframe_template/>.

BIN
docs/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/icon-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

BIN
docs/icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
docs/icon_ios_touch_192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

173
docs/index.html Normal file
View file

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<title>eframe template</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
<link rel="apple-touch-icon" href="/icon_ios_touch_192.png">
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
}
/* Position canvas in center-top: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, 0%);
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<canvas id="the_canvas_id"></canvas>
<div class="centered" id="center_text">
<p style="font-size:16px">
Loading…
</p>
<div class="lds-dual-ring"></div>
</div>
<script>
// The `--no-modules`-generated JS from `wasm-bindgen` attempts to use
// `WebAssembly.instantiateStreaming` to instantiate the wasm module,
// but this doesn't work with `file://` urls. This example is frequently
// viewed by simply opening `index.html` in a browser (with a `file://`
// url), so it would fail if we were to call this function!
//
// Work around this for now by deleting the function to ensure that the
// `no_modules.js` script doesn't have access to it. You won't need this
// hack when deploying over HTTP.
delete WebAssembly.instantiateStreaming;
</script>
<!-- this is the JS generated by the `wasm-bindgen` CLI tool -->
<script src="plotter.js"></script>
<script>
// We'll defer our execution until the wasm is ready to go.
// Here we tell bindgen the path to the wasm file so it can start
// initialization and return to us a promise when it's done.
console.debug("loading wasm…");
wasm_bindgen("./plotter_bg.wasm")
.then(on_wasm_loaded)
.catch(on_wasm_error);
function on_wasm_loaded() {
console.debug("wasm loaded. starting app…");
// This call installs a bunch of callbacks and then returns:
wasm_bindgen.start("the_canvas_id");
console.debug("app started.");
document.getElementById("center_text").remove();
}
function on_wasm_error(error) {
console.error("Failed to start: " + error);
document.getElementById("center_text").innerHTML = `
<p>
An error occurred during loading:
</p>
<p style="font-family:Courier New">
${error}
</p>
<p style="font-size:14px">
Make sure you use a modern browser with WebGL and WASM enabled.
</p>`;
}
</script>
<!--Register Service Worker-->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

27
docs/manifest.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "2b2t queue stats",
"short_name": "2b-queue",
"icons": [{
"src": "./icon-256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "./maskable_icon_x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "./icon-1024.png",
"sizes": "1024x1024",
"type": "image/png"
}
],
"lang": "en-US",
"id": "/index.html",
"start_url": "./index.html",
"display": "standalone",
"background_color": "black",
"theme_color": "black"
}

BIN
docs/maskable_icon_x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

1468
docs/plotter.js Normal file

File diff suppressed because it is too large Load diff

BIN
docs/plotter_bg.wasm Normal file

Binary file not shown.

25
docs/sw.js Normal file
View file

@ -0,0 +1,25 @@
var cacheName = 'egui-template-pwa';
var filesToCache = [
'./',
'./index.html',
'./eframe_template.js',
'./eframe_template_bg.wasm',
];
/* Start the service worker and cache all of the app's content */
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll(filesToCache);
})
);
});
/* Serve cached content when offline */
self.addEventListener('fetch', function (e) {
e.respondWith(
caches.match(e.request).then(function (response) {
return response || fetch(e.request);
})
);
});

143
src/app/datasource.rs Normal file
View file

@ -0,0 +1,143 @@
use std::sync::{Arc, Mutex};
use rand::Rng;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use eframe::egui::plot::Value;
struct DataSource {
data : Arc<Mutex<Vec<Value>>>,
}
impl DataSource {
fn new() -> Self {
Self{ data: Arc::new(Mutex::new(Vec::new())) }
}
fn view(&self) -> Vec<Value> { // TODO handle errors
return self.data.lock().unwrap().clone();
}
}
pub trait Data {
fn load(&mut self, url:&str);
fn view(&self) -> Vec<Value>;
}
pub struct TpsData {
ds: DataSource,
load_interval : i64,
last_load : DateTime<Utc>,
}
#[derive(Serialize, Deserialize)]
struct TpsResponseData {
tps: f64
}
impl TpsData {
pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
}
}
impl Data for TpsData{
fn load(&mut self, url:&str) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now();
let ds_data = self.ds.data.clone();
let request = ehttp::Request::get(format!("{}/tps", url));
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let data : TpsResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap();
ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.tps});
});
}
fn view(&self) -> Vec<Value> { self.ds.view() }
}
pub struct ChatData {
ds : DataSource,
load_interval : i64,
last_load : DateTime<Utc>,
}
#[derive(Serialize, Deserialize)]
struct ChatResponseData {
volume: f64
}
impl ChatData {
pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
}
}
impl Data for ChatData{
fn load(&mut self, url:&str) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now();
let ds_data = self.ds.data.clone();
let request = ehttp::Request::get(format!("{}/chat_activity", url));
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let data : ChatResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap();
ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.volume});
});
}
fn view(&self) -> Vec<Value> { self.ds.view() }
}
pub struct PlayerCountData {
ds : DataSource,
load_interval : i64,
last_load : DateTime<Utc>,
}
#[derive(Serialize, Deserialize)]
struct PlayerCountResponseData {
count: i32
}
impl PlayerCountData {
pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
}
}
impl Data for PlayerCountData{
fn load(&mut self, url:&str) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now();
let ds_data = self.ds.data.clone();
let request = ehttp::Request::get(format!("{}/chat_activity", url));
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let data : PlayerCountResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap();
ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.count as f64});
});
}
fn view(&self) -> Vec<Value> { self.ds.view() }
}
pub struct RandomData {
ds : DataSource,
load_interval : i64,
last_load : DateTime<Utc>,
rng: rand::rngs::ThreadRng,
}
impl RandomData {
pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval, rng : rand::thread_rng() }
}
}
impl Data for RandomData{
fn load(&mut self, _url:&str) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now();
self.ds.data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:self.rng.gen()});
}
fn view(&self) -> Vec<Value> { self.ds.view() }
}

70
src/app/mod.rs Normal file
View file

@ -0,0 +1,70 @@
mod datasource;
use chrono::{DateTime, NaiveDateTime, Utc};
use datasource::{ChatData, PlayerCountData, TpsData, Data, RandomData};
use eframe::egui;
use eframe::egui::plot::{Line, Plot, Value, Values};
pub struct App {
player_count: PlayerCountData,
tps: TpsData,
chat: ChatData,
rand: RandomData,
sync_time:bool,
}
impl App {
pub fn new(_cc: &eframe::CreationContext) -> Self {
Self {
player_count: PlayerCountData::new(60),
tps: TpsData::new(30),
chat: ChatData::new(15),
rand: RandomData::new(1),
sync_time: false,
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.rand.load("");
egui::TopBottomPanel::top("??? wtf").show(ctx, |ui| {
ui.horizontal(|ui| {
egui::widgets::global_dark_light_mode_switch(ui);
ui.heading("nnbot dashboard");
ui.checkbox(&mut self.sync_time, "Lock X to now");
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
if ui.button("x").clicked() {
frame.quit();
}
});
});
});
egui::CentralPanel::default().show(ctx, |ui| {
let mut p = Plot::new("test").x_axis_formatter(|x, _range| {
format!(
"{}",
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(x as i64, 0), Utc)
.format("%Y/%m/%d %H:%M:%S")
)
}).center_x_axis(false);
if self.sync_time {
p = p.include_x(Utc::now().timestamp() as f64);
}
p.show(ui, |plot_ui| {
plot_ui.line(
Line::new(Values::from_values(self.player_count.view())).name("Player Count"),
);
plot_ui.line(Line::new(Values::from_values(self.tps.view())).name("TPS over 15s"));
plot_ui.line(Line::new(Values::from_values(self.rand.view())).name("Random Data"));
plot_ui.line(
Line::new(Values::from_values(self.chat.view()))
.name("Chat messages per minute"),
);
});
});
}
}

20
src/lib.rs Normal file
View file

@ -0,0 +1,20 @@
mod app;
pub use app::App;
// When compiling for web:
#[cfg(target_arch = "wasm32")]
use eframe::wasm_bindgen::{self, prelude::*};
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
eframe::start_web(canvas_id, Box::new(|cc| Box::new(App::new(cc))))
}

15
src/main.rs Normal file
View file

@ -0,0 +1,15 @@
mod app;
use app::App;
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() {
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"2b2t queue stats",
native_options,
Box::new(|cc| Box::new(App::new(cc))),
);
}